diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..7f35d2cb1 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-format": { + "version": "5.1.250801", + "commands": [ + "dotnet-format" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig index 1fbf4e5d8..1d09080d9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,7 @@ root = true # Don't use tabs for indentation. [*] indent_style = space +end_of_line = lf # (Please don't specify an indent_size here; that has too many unintended consequences.) # Code files diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..e6dc4e220 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# .NET format https://github.com/bitwarden/mobile/pull/1738 +04539af2a66668b6e85476d5cf318c9150ec4357 diff --git a/.gitattributes b/.gitattributes index 1ff0c4230..6f3fcbddb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### -* text=auto +* text=auto eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 807709ec5..2ba0505a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,11 +21,6 @@ -## Testing requirements - - - - ## Before you submit - [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required) - [ ] This change requires a **documentation update** (notify the documentation team) diff --git a/.github/secrets/dist_autofill.mobileprovision.gpg b/.github/secrets/dist_autofill.mobileprovision.gpg index 25cc64b08..1143948a8 100644 Binary files a/.github/secrets/dist_autofill.mobileprovision.gpg and b/.github/secrets/dist_autofill.mobileprovision.gpg differ diff --git a/.github/secrets/dist_bitwarden.mobileprovision.gpg b/.github/secrets/dist_bitwarden.mobileprovision.gpg index 54bcdf626..979b07329 100644 Binary files a/.github/secrets/dist_bitwarden.mobileprovision.gpg and b/.github/secrets/dist_bitwarden.mobileprovision.gpg differ diff --git a/.github/secrets/dist_extension.mobileprovision.gpg b/.github/secrets/dist_extension.mobileprovision.gpg index 7fbd696b9..5ba738ada 100644 Binary files a/.github/secrets/dist_extension.mobileprovision.gpg and b/.github/secrets/dist_extension.mobileprovision.gpg differ diff --git a/.github/secrets/dist_share_extension.mobileprovision.gpg b/.github/secrets/dist_share_extension.mobileprovision.gpg index 0fad3826d..bcfac9f53 100644 Binary files a/.github/secrets/dist_share_extension.mobileprovision.gpg and b/.github/secrets/dist_share_extension.mobileprovision.gpg differ diff --git a/.github/secrets/iphone-distribution-cert.p12.gpg b/.github/secrets/iphone-distribution-cert.p12.gpg index 36a5c8888..0fb7ebd94 100644 Binary files a/.github/secrets/iphone-distribution-cert.p12.gpg and b/.github/secrets/iphone-distribution-cert.p12.gpg differ diff --git a/.github/secrets/play_creds.json.gpg b/.github/secrets/play_creds.json.gpg index d30fd9c1c..b1a1ee447 100644 Binary files a/.github/secrets/play_creds.json.gpg and b/.github/secrets/play_creds.json.gpg differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7578e3849..104fa1176 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,10 @@ on: branches-ignore: - 'l10n_master' - 'gh-pages' + paths-ignore: + - '.github/workflows/**' + workflow_dispatch: + inputs: {} jobs: cloc: @@ -13,7 +17,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Set up CLOC run: | @@ -32,7 +36,7 @@ jobs: hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }} steps: - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Check if special branches exist id: branch-check @@ -43,7 +47,7 @@ jobs: echo "::set-output name=rc_branch_exists::0" fi - if [[ $(git ls-remote --heads origin hotfix) ]]; then + if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then echo "::set-output name=hotfix_branch_exists::1" else echo "::set-output name=hotfix_branch_exists::0" @@ -57,7 +61,7 @@ jobs: needs: setup steps: - name: Set up MSBuild - uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d # v1 + uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab - name: Print environment run: | @@ -68,7 +72,7 @@ jobs: echo "GitHub event: $GITHUB_EVENT" - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Decrypt secrets env: @@ -101,6 +105,14 @@ jobs: - name: Restore packages run: nuget restore + - name: Restore tools + run: dotnet tool restore + shell: pwsh + + - name: Verify Format + run: dotnet tool run dotnet-format --check + shell: pwsh + - name: Run Core tests run: dotnet test test/Core.Test/Core.Test.csproj @@ -163,14 +175,14 @@ jobs: shell: pwsh - name: Upload Play Store .aab artifact - uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4 + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.aab path: ./com.x8bit.bitwarden.aab if-no-files-found: error - name: Upload Play Store .apk artifact - uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4 + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.apk path: ./com.x8bit.bitwarden.apk @@ -182,9 +194,9 @@ jobs: && needs.setup.outputs.rc_branch_exists == 0 && needs.setup.outputs.hotfix_branch_exists == 0) || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) - || github.ref == 'refs/heads/hotfix' + || github.ref == 'refs/heads/hotfix-rc' run: | - PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp2.0/Publisher.dll" + PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll" CREDS_PATH="$HOME/secrets/play_creds.json" AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab" TRACK="internal" @@ -198,7 +210,7 @@ jobs: runs-on: windows-2019 steps: - name: Set up MSBuild - uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d # v1 + uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab - name: Print environment run: | @@ -209,7 +221,7 @@ jobs: echo "GitHub event: $GITHUB_EVENT" - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Decrypt secrets env: @@ -237,6 +249,7 @@ jobs: run: | $androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj"); $appPath = $($env:GITHUB_WORKSPACE + "/src/App/App.csproj"); + $corePath = $($env:GITHUB_WORKSPACE + "/src/Core/Core.csproj"); $androidManifest = $($env:GITHUB_WORKSPACE + "/src/Android/Properties/AndroidManifest.xml"); @@ -292,16 +305,16 @@ jobs: $xml.Save($androidPath); Write-Output "########################################" - Write-Output "##### Uninstall from App.csproj" + Write-Output "##### Uninstall from Core.csproj" Write-Output "########################################" $xml=New-Object XML; - $xml.Load($appPath); + $xml.Load($corePath); $appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']"); $appCenterNode.ParentNode.RemoveChild($appCenterNode); - $xml.Save($appPath); + $xml.Save($corePath); shell: pwsh - name: Restore packages @@ -343,7 +356,7 @@ jobs: shell: pwsh - name: Upload F-Droid .apk artifact - uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4 + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden-fdroid.apk path: ./com.x8bit.bitwarden-fdroid.apk @@ -364,7 +377,19 @@ jobs: echo "GitHub event: $GITHUB_EVENT" - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + + - name: Login to Azure - Prod Subscription + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f + with: + keyvault: "bitwarden-prod-kv" + secrets: "appcenter-ios-token" - name: Decrypt secrets env: @@ -383,7 +408,8 @@ jobs: gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output $HOME/secrets/dist_extension.mobileprovision ./.github/secrets/dist_extension.mobileprovision.gpg gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - --output $HOME/secrets/dist_share_extension.mobileprovision ./.github/secrets/dist_share_extension.mobileprovision.gpg + --output $HOME/secrets/dist_share_extension.mobileprovision \ + ./.github/secrets/dist_share_extension.mobileprovision.gpg shell: bash - name: Increment version @@ -479,20 +505,51 @@ jobs: -exportOptionsPlist $EXPORT_OPTIONS_PATH shell: bash - - name: Upload App Store .ipa artifact - uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4 + - name: Copy all dSYMs files to upload + run: | + ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs" + EXPORT_PATH="./bitwarden-export" + + cp -r $ARCHIVE_DSYMS_PATH $EXPORT_PATH + shell: bash + + - name: Upload App Store .ipa & dSYMs artifacts + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: - name: Bitwarden.ipa - path: ./bitwarden-export/Bitwarden.ipa + name: Bitwarden iOS + path: | + ./bitwarden-export/Bitwarden.ipa + ./bitwarden-export/dSYMs/*.* if-no-files-found: error + - name: Install AppCenter CLI + if: | + (github.ref == 'refs/heads/master' + && needs.setup.outputs.rc_branch_exists == 0 + && needs.setup.outputs.hotfix_branch_exists == 0) + || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) + || github.ref == 'refs/heads/hotfix-rc' + run: npm install -g appcenter-cli + + - name: Upload dSYMs to App Center + if: | + (github.ref == 'refs/heads/master' + && needs.setup.outputs.rc_branch_exists == 0 + && needs.setup.outputs.hotfix_branch_exists == 0) + || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) + || github.ref == 'refs/heads/hotfix-rc' + env: + APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }} + run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN + shell: bash + - name: Deploy to App Store if: | (github.ref == 'refs/heads/master' && needs.setup.outputs.rc_branch_exists == 0 && needs.setup.outputs.hotfix_branch_exists == 0) || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) - || github.ref == 'refs/heads/hotfix' + || github.ref == 'refs/heads/hotfix-rc' env: APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} @@ -514,22 +571,22 @@ jobs: _CROWDIN_PROJECT_ID: "269690" steps: - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Login to Azure - uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf with: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403 + uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f with: keyvault: "bitwarden-prod-kv" secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 + uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} @@ -555,7 +612,7 @@ jobs: if: | (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') - || (github.ref == 'refs/heads/hotfix') + || (github.ref == 'refs/heads/hotfix-rc') env: CLOC_STATUS: ${{ needs.cloc.result }} ANDROID_STATUS: ${{ needs.android.result }} @@ -576,21 +633,21 @@ jobs: fi - name: Login to Azure - Prod Subscription - uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf if: failure() with: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403 + uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f if: failure() with: keyvault: "bitwarden-prod-kv" secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2 + uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml new file mode 100644 index 000000000..0a63c70e4 --- /dev/null +++ b/.github/workflows/enforce-labels.yml @@ -0,0 +1,16 @@ +--- +name: Enforce PR labels + +on: + pull_request: + types: [labeled, unlabeled, opened, edited, synchronize] +jobs: + enforce-label: + name: EnforceLabel + runs-on: ubuntu-20.04 + steps: + - name: Enforce Label + uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb + with: + BANNED_LABELS: "hold" + BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66dfb66c9..631d08a25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ on: options: - Initial Release - Redeploy + - Dry Run jobs: release: @@ -21,21 +22,23 @@ jobs: branch-name: ${{ steps.branch.outputs.branch-name }} steps: - name: Branch check + if: github.event.inputs.release_type != 'Dry Run' run: | - if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then echo "===================================" - echo "[!] Can only release from the 'rc' or 'hotfix' branches" + echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches" echo "===================================" exit 1 fi - name: Checkout repo - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 - name: Retrieve Mobile release version id: retrieve-mobile-version run: | - ver=$(sed -E -n '/^("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } if (_broadcasterService == null) { @@ -460,12 +459,12 @@ namespace Bit.Droid.Accessibility if (_lastSettingsReload == null || (now - _lastSettingsReload.Value) > _settingsReloadSpan) { _lastSettingsReload = now; - var uris = await _storageService.GetAsync>(Constants.AutofillBlacklistedUrisKey); + var uris = await _stateService.GetAutofillBlacklistedUrisAsync(); if (uris != null) { _blacklistedUris = new HashSet(uris); } - var isAutoFillTileAdded = await _storageService.GetAsync(Constants.AutofillTileAdded); + var isAutoFillTileAdded = await _stateService.GetAutofillTileAddedAsync(); AccessibilityHelpers.IsAutofillTileAdded = isAutoFillTileAdded.GetValueOrDefault(); } } diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 462afcaf9..94691d6a3 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -84,7 +84,7 @@ - 1.7.0 + 1.7.2 122.0.0 @@ -145,12 +145,12 @@ - + @@ -171,7 +171,8 @@ - + + diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index ab10aa005..4549e56b8 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -51,6 +51,8 @@ namespace Bit.Droid.Autofill // - ... to keep this list in sync with values in AccessibilityHelpers.SupportedBrowsers [Section A], too. public static HashSet CompatBrowsers = new HashSet { + "alook.browser", + "alook.browser.google", "com.amazon.cloud9", "com.android.browser", "com.android.chrome", diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index 0d0b1cc07..07171eb9e 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -1,4 +1,7 @@ -using Android; +using System; +using System.Collections.Generic; +using System.Linq; +using Android; using Android.App; using Android.Content; using Android.OS; @@ -9,12 +12,6 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; -#if !FDROID -using Microsoft.AppCenter.Crashes; -#endif -using System; -using System.Collections.Generic; -using System.Linq; namespace Bit.Droid.Autofill { @@ -26,9 +23,9 @@ namespace Bit.Droid.Autofill { private ICipherService _cipherService; private IVaultTimeoutService _vaultTimeoutService; - private IStorageService _storageService; private IPolicyService _policyService; - private IUserService _userService; + private IStateService _stateService; + private LazyResolve _logger = new LazyResolve("logger"); public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) @@ -44,18 +41,18 @@ namespace Bit.Droid.Autofill var parser = new Parser(structure, ApplicationContext); parser.Parse(); - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - var shouldAutofill = await parser.ShouldAutofillAsync(_storageService); + var shouldAutofill = await parser.ShouldAutofillAsync(_stateService); if (!shouldAutofill) { return; } - var inlineAutofillEnabled = await _storageService.GetAsync(Constants.InlineAutofillEnabledKey) ?? true; + var inlineAutofillEnabled = await _stateService.GetInlineAutofillEnabledAsync() ?? true; if (_vaultTimeoutService == null) { @@ -76,7 +73,7 @@ namespace Bit.Droid.Autofill // build response var response = AutofillHelpers.CreateFillResponse(parser, items, locked, inlineAutofillEnabled, request); - var disableSavePrompt = await _storageService.GetAsync(Constants.AutofillDisableSavePromptKey); + var disableSavePrompt = await _stateService.GetAutofillDisableSavePromptAsync(); if (!disableSavePrompt.GetValueOrDefault()) { AutofillHelpers.AddSaveInfo(parser, request, response, parser.FieldCollection); @@ -85,9 +82,7 @@ namespace Bit.Droid.Autofill } catch (Exception e) { -#if !FDROID - Crashes.TrackError(e); -#endif + _logger.Value.Exception(e); } } @@ -101,12 +96,12 @@ namespace Bit.Droid.Autofill return; } - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - var disableSavePrompt = await _storageService.GetAsync(Constants.AutofillDisableSavePromptKey); + var disableSavePrompt = await _stateService.GetAutofillDisableSavePromptAsync(); if (disableSavePrompt.GetValueOrDefault()) { return; @@ -161,9 +156,7 @@ namespace Bit.Droid.Autofill } catch (Exception e) { -#if !FDROID - Crashes.TrackError(e); -#endif + _logger.Value.Exception(e); } } } diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs index d097d4d1f..45a2fdb0b 100644 --- a/src/Android/Autofill/Parser.cs +++ b/src/Android/Autofill/Parser.cs @@ -80,13 +80,13 @@ namespace Bit.Droid.Autofill } } - public async Task ShouldAutofillAsync(IStorageService storageService) + public async Task ShouldAutofillAsync(IStateService stateService) { var fillable = !string.IsNullOrWhiteSpace(Uri) && !AutofillHelpers.BlacklistedUris.Contains(Uri) && FieldCollection != null && FieldCollection.Fillable; if (fillable) { - var blacklistedUris = await storageService.GetAsync>(Constants.AutofillBlacklistedUrisKey); + var blacklistedUris = await stateService.GetAutofillBlacklistedUrisAsync(); if (blacklistedUris != null && blacklistedUris.Count > 0) { fillable = !new HashSet(blacklistedUris).Contains(Uri); diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index c44c83a3a..af98b744c 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -32,7 +32,7 @@ namespace Bit.Droid private IDeviceActionService _deviceActionService; private IMessagingService _messagingService; private IBroadcasterService _broadcasterService; - private IUserService _userService; + private IStateService _stateService; private IAppIdService _appIdService; private IEventService _eventService; private PendingIntent _eventUploadPendingIntent; @@ -53,7 +53,7 @@ namespace Bit.Droid _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _eventService = ServiceContainer.Resolve("eventService"); @@ -69,16 +69,20 @@ namespace Bit.Droid Window.AddFlags(Android.Views.WindowManagerFlags.Secure); } -#if !FDROID - var appCenterHelper = new AppCenterHelper(_appIdService, _userService); - var appCenterTask = appCenterHelper.InitAsync(); -#endif + ServiceContainer.Resolve("logger").InitAsync(); + + var toplayout = Window?.DecorView?.RootView; + if (toplayout != null) + { + toplayout.FilterTouchesWhenObscured = true; + } Xamarin.Essentials.Platform.Init(this, savedInstanceState); Xamarin.Forms.Forms.Init(this, savedInstanceState); _appOptions = GetOptions(); LoadApplication(new App.App(_appOptions)); + _broadcasterService.Subscribe(_activityKey, (message) => { if (message.Command == "startEventTimer") @@ -375,7 +379,7 @@ namespace Bit.Droid { Window?.SetStatusBarColor(ThemeHelpers.NavBarBackgroundColor); Window?.DecorView.SetBackgroundColor(ThemeHelpers.BackgroundColor); - ThemeHelpers.SetAppearance(ThemeManager.GetTheme(true), ThemeManager.OsDarkModeEnabled()); + ThemeHelpers.SetAppearance(ThemeManager.GetTheme(), ThemeManager.OsDarkModeEnabled()); } private void ExitApp() diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 1957f70a2..aadd11564 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -53,7 +53,8 @@ namespace Bit.Droid ServiceContainer.Resolve("apiService"), ServiceContainer.Resolve("messagingService"), ServiceContainer.Resolve("platformUtilsService"), - ServiceContainer.Resolve("deviceActionService")); + ServiceContainer.Resolve("deviceActionService"), + ServiceContainer.Resolve("logger")); ServiceContainer.Register("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner); var verificationActionsFlowHelper = new VerificationActionsFlowHelper( @@ -87,7 +88,14 @@ namespace Bit.Droid private void RegisterLocalServices() { - ServiceContainer.Register("logService", new AndroidLogService()); + ServiceContainer.Register("nativeLogService", new AndroidLogService()); +#if FDROID + ServiceContainer.Register("logger", new StubLogger()); +#elif DEBUG + ServiceContainer.Register("logger", DebugLogger.Instance); +#else + ServiceContainer.Register("logger", Logger.Instance); +#endif // Note: This might cause a race condition. Investigate more. Task.Run(() => @@ -113,13 +121,16 @@ namespace Bit.Droid var secureStorageService = new SecureStorageService(); var cryptoPrimitiveService = new CryptoPrimitiveService(); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); - var deviceActionService = new DeviceActionService(mobileStorageService, messagingService, + var stateService = new StateService(mobileStorageService, secureStorageService); + var stateMigrationService = + new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); + var deviceActionService = new DeviceActionService(stateService, messagingService, broadcasterService, () => ServiceContainer.Resolve("eventService")); var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, broadcasterService); var biometricService = new BiometricService(); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); - var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService); + var cryptoService = new CryptoService(stateService, cryptoFunctionService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); ServiceContainer.Register("broadcasterService", broadcasterService); @@ -129,7 +140,9 @@ namespace Bit.Droid ServiceContainer.Register("cryptoPrimitiveService", cryptoPrimitiveService); ServiceContainer.Register("storageService", mobileStorageService); ServiceContainer.Register("secureStorageService", secureStorageService); - ServiceContainer.Register("clipboardService", new ClipboardService(mobileStorageService)); + ServiceContainer.Register("stateService", stateService); + ServiceContainer.Register("stateMigrationService", stateMigrationService); + ServiceContainer.Register("clipboardService", new ClipboardService(stateService)); ServiceContainer.Register("deviceActionService", deviceActionService); ServiceContainer.Register("platformUtilsService", platformUtilsService); ServiceContainer.Register("biometricService", biometricService); @@ -148,7 +161,7 @@ namespace Bit.Droid ServiceContainer.Register( "pushNotificationListenerService", notificationListenerService); var androidPushNotificationService = new AndroidPushNotificationService( - mobileStorageService, notificationListenerService); + stateService, notificationListenerService); ServiceContainer.Register( "pushNotificationService", androidPushNotificationService); #endif @@ -164,10 +177,6 @@ namespace Bit.Droid private async Task BootstrapAsync() { - var disableFavicon = await ServiceContainer.Resolve("storageService") - .GetAsync(Constants.DisableFaviconKey); - await ServiceContainer.Resolve("stateService").SaveAsync( - Constants.DisableFaviconKey, disableFavicon); await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); } } diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index ac94e44cd..f6afe8c0b 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/Android/Push/FirebaseMessagingService.cs b/src/Android/Push/FirebaseMessagingService.cs index 5351436c2..676390aef 100644 --- a/src/Android/Push/FirebaseMessagingService.cs +++ b/src/Android/Push/FirebaseMessagingService.cs @@ -16,10 +16,10 @@ namespace Bit.Droid.Push { public async override void OnNewToken(string token) { - var storageService = ServiceContainer.Resolve("storageService"); + var stateService = ServiceContainer.Resolve("stateService"); var pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); - await storageService.SaveAsync(Core.Constants.PushRegisteredTokenKey, token); + await stateService.SetPushRegisteredTokenAsync(token); await pushNotificationService.RegisterAsync(); } diff --git a/src/Android/Receivers/PackageReplacedReceiver.cs b/src/Android/Receivers/PackageReplacedReceiver.cs index 2af350320..a4a03f9c1 100644 --- a/src/Android/Receivers/PackageReplacedReceiver.cs +++ b/src/Android/Receivers/PackageReplacedReceiver.cs @@ -1,5 +1,4 @@ -using System; -using Android.App; +using Android.App; using Android.Content; using Bit.App.Abstractions; using Bit.App.Utilities; @@ -14,9 +13,10 @@ namespace Bit.Droid.Receivers { public override async void OnReceive(Context context, Intent intent) { - var storageService = ServiceContainer.Resolve("storageService"); - await AppHelpers.PerformUpdateTasksAsync(ServiceContainer.Resolve("syncService"), - ServiceContainer.Resolve("deviceActionService"), storageService); + await AppHelpers.PerformUpdateTasksAsync( + ServiceContainer.Resolve("syncService"), + ServiceContainer.Resolve("deviceActionService"), + ServiceContainer.Resolve("stateService")); } } } diff --git a/src/Android/Renderers/CustomPageRenderer.cs b/src/Android/Renderers/CustomPageRenderer.cs new file mode 100644 index 000000000..5dec9821a --- /dev/null +++ b/src/Android/Renderers/CustomPageRenderer.cs @@ -0,0 +1,31 @@ +using System; +using Android.App; +using Android.Content; +using AndroidX.AppCompat.Widget; +using Bit.App.Resources; +using Bit.Droid.Renderers; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(ContentPage), typeof(CustomPageRenderer))] +namespace Bit.Droid.Renderers +{ + public class CustomPageRenderer : PageRenderer + { + public CustomPageRenderer(Context context) : base(context) + { + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + + Activity context = (Activity)this.Context; + var toolbar = context.FindViewById(Resource.Id.toolbar); + if(toolbar != null) + { + toolbar.NavigationContentDescription = AppResources.TapToGoBack; + } + } + } +} diff --git a/src/Android/Renderers/CustomTabbedRenderer.cs b/src/Android/Renderers/CustomTabbedRenderer.cs index ee11f56a2..dea31033a 100644 --- a/src/Android/Renderers/CustomTabbedRenderer.cs +++ b/src/Android/Renderers/CustomTabbedRenderer.cs @@ -1,7 +1,9 @@ using Android.Content; using Android.Views; +using Bit.App.Pages; using Bit.Droid.Renderers; using Google.Android.Material.BottomNavigation; +using Google.Android.Material.Navigation; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Xamarin.Forms.Platform.Android.AppCompat; @@ -9,7 +11,7 @@ using Xamarin.Forms.Platform.Android.AppCompat; [assembly: ExportRenderer(typeof(TabbedPage), typeof(CustomTabbedRenderer))] namespace Bit.Droid.Renderers { - public class CustomTabbedRenderer : TabbedPageRenderer, BottomNavigationView.IOnNavigationItemReselectedListener + public class CustomTabbedRenderer : TabbedPageRenderer, NavigationBarView.IOnItemReselectedListener { private TabbedPage _page; @@ -21,7 +23,7 @@ namespace Bit.Droid.Renderers if (e.NewElement != null) { _page = e.NewElement; - GetBottomNavigationView()?.SetOnNavigationItemReselectedListener(this); + GetBottomNavigationView()?.SetOnItemReselectedListener(this); } else { @@ -53,6 +55,10 @@ namespace Bit.Droid.Renderers { if (_page?.CurrentPage?.Navigation != null && _page.CurrentPage.Navigation.NavigationStack.Count > 0) { + if (_page is TabsPage tabsPage) + { + tabsPage.OnPageReselected(); + } Device.BeginInvokeOnMainThread(async () => await _page.CurrentPage.Navigation.PopToRootAsync()); } } diff --git a/src/Android/Resources/drawable/cog_environment.xml b/src/Android/Resources/drawable/cog_environment.xml new file mode 100644 index 000000000..6e5b4ff76 --- /dev/null +++ b/src/Android/Resources/drawable/cog_environment.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/Android/Resources/drawable/cog.xml b/src/Android/Resources/drawable/cog_settings.xml similarity index 99% rename from src/Android/Resources/drawable/cog.xml rename to src/Android/Resources/drawable/cog_settings.xml index 311d0ad0c..b8c3ba603 100644 --- a/src/Android/Resources/drawable/cog.xml +++ b/src/Android/Resources/drawable/cog_settings.xml @@ -6,4 +6,4 @@ - + \ No newline at end of file diff --git a/src/Android/Resources/xml/autofillservice.xml b/src/Android/Resources/xml/autofillservice.xml index 20432c86b..c65812815 100644 --- a/src/Android/Resources/xml/autofillservice.xml +++ b/src/Android/Resources/xml/autofillservice.xml @@ -11,6 +11,12 @@ --> + + diff --git a/src/Android/Services/AndroidLogService.cs b/src/Android/Services/AndroidLogService.cs index ead61510d..8d7cdac54 100644 --- a/src/Android/Services/AndroidLogService.cs +++ b/src/Android/Services/AndroidLogService.cs @@ -3,7 +3,7 @@ using System; namespace Bit.Core.Services { - public class AndroidLogService : ILogService + public class AndroidLogService : INativeLogService { private static readonly string _tag = "BITWARDEN"; diff --git a/src/Android/Services/AndroidPushNotificationService.cs b/src/Android/Services/AndroidPushNotificationService.cs index 097c5f708..e871393f6 100644 --- a/src/Android/Services/AndroidPushNotificationService.cs +++ b/src/Android/Services/AndroidPushNotificationService.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using AndroidX.Core.App; using Bit.App.Abstractions; -using Bit.Core; using Bit.Core.Abstractions; using Xamarin.Forms; @@ -11,14 +10,14 @@ namespace Bit.Droid.Services { public class AndroidPushNotificationService : IPushNotificationService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IPushNotificationListenerService _pushNotificationListenerService; public AndroidPushNotificationService( - IStorageService storageService, + IStateService stateService, IPushNotificationListenerService pushNotificationListenerService) { - _storageService = storageService; + _stateService = stateService; _pushNotificationListenerService = pushNotificationListenerService; } @@ -26,12 +25,12 @@ namespace Bit.Droid.Services public async Task GetTokenAsync() { - return await _storageService.GetAsync(Constants.PushCurrentTokenKey); + return await _stateService.GetPushCurrentTokenAsync(); } public async Task RegisterAsync() { - var registeredToken = await _storageService.GetAsync(Constants.PushRegisteredTokenKey); + var registeredToken = await _stateService.GetPushRegisteredTokenAsync(); var currentToken = await GetTokenAsync(); if (!string.IsNullOrWhiteSpace(registeredToken) && registeredToken != currentToken) { @@ -39,7 +38,7 @@ namespace Bit.Droid.Services } else { - await _storageService.SaveAsync(Constants.PushLastRegistrationDateKey, DateTime.UtcNow); + await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); } } diff --git a/src/Android/Services/BiometricService.cs b/src/Android/Services/BiometricService.cs index aa23477ba..e14ae24ed 100644 --- a/src/Android/Services/BiometricService.cs +++ b/src/Android/Services/BiometricService.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Android.OS; using Android.Security.Keystore; using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; using Java.Security; using Javax.Crypto; @@ -43,23 +42,22 @@ namespace Bit.Droid.Services public Task ValidateIntegrityAsync(string bioIntegrityKey = null) { - // bioIntegrityKey used in iOS only if (Build.VERSION.SdkInt < BuildVersionCodes.M) { return Task.FromResult(true); } - _keystore.Load(null); - IKey key = _keystore.GetKey(KeyName, null); - Cipher cipher = Cipher.GetInstance(Transformation); - - if (key == null || cipher == null) - { - return Task.FromResult(true); - } - try { + _keystore.Load(null); + var key = _keystore.GetKey(KeyName, null); + var cipher = Cipher.GetInstance(Transformation); + + if (key == null || cipher == null) + { + return Task.FromResult(true); + } + cipher.Init(CipherMode.EncryptMode, key); } catch (KeyPermanentlyInvalidatedException e) @@ -75,6 +73,7 @@ namespace Bit.Droid.Services catch (InvalidKeyException e) { // Fallback for old bitwarden users without a key + LoggerHelper.LogEvenIfCantBeResolved(e); CreateKey(); } @@ -95,10 +94,11 @@ namespace Bit.Droid.Services keyGen.Init(keyGenSpec); keyGen.GenerateKey(); } - catch + catch (Exception e) { // Catch silently to allow biometrics to function on devices that are in a state where key generation // is not functioning + LoggerHelper.LogEvenIfCantBeResolved(e); } } } diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 82f1c21db..07e720c57 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -12,12 +12,12 @@ namespace Bit.Droid.Services { public class ClipboardService : IClipboardService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly Lazy _clearClipboardPendingIntent; - public ClipboardService(IStorageService storageService) + public ClipboardService(IStateService stateService) { - _storageService = storageService; + _stateService = stateService; _clearClipboardPendingIntent = new Lazy(() => PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity, @@ -39,7 +39,7 @@ namespace Bit.Droid.Services if (clearMs < 0) { // if not set then we need to check if the user set this config - var clearSeconds = await _storageService.GetAsync(Constants.ClearClipboardKey); + var clearSeconds = await _stateService.GetClearClipboardAsync(); if (clearSeconds != null) { clearMs = clearSeconds.Value * 1000; diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index c83689c97..37d90d0d4 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -35,7 +35,7 @@ namespace Bit.Droid.Services { public class DeviceActionService : IDeviceActionService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IMessagingService _messagingService; private readonly IBroadcasterService _broadcasterService; private readonly Func _eventServiceFunc; @@ -47,12 +47,12 @@ namespace Bit.Droid.Services private string _userAgent; public DeviceActionService( - IStorageService storageService, + IStateService stateService, IMessagingService messagingService, IBroadcasterService broadcasterService, Func eventServiceFunc) { - _storageService = storageService; + _stateService = stateService; _messagingService = messagingService; _broadcasterService = broadcasterService; _eventServiceFunc = eventServiceFunc; @@ -333,7 +333,7 @@ namespace Bit.Droid.Services try { DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir); - await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow); + await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow); } catch (Exception) { } } @@ -674,7 +674,7 @@ namespace Bit.Droid.Services else { var data = new Intent(); - if (cipher == null) + if (cipher?.Login == null) { data.PutExtra("canceled", "true"); } @@ -734,6 +734,11 @@ namespace Bit.Droid.Services return Accessibility.AccessibilityHelpers.OverlayPermitted(); } + public bool HasAutofillService() + { + return true; + } + public void OpenAccessibilityOverlayPermissionSettings() { var activity = (MainActivity)CrossCurrentActivity.Current.Activity; @@ -916,9 +921,8 @@ namespace Bit.Droid.Services { if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp)) { - var userService = ServiceContainer.Resolve("userService"); - var autoCopyDisabled = await _storageService.GetAsync(Constants.DisableAutoTotpCopyKey); - var canAccessPremium = await userService.CanAccessPremiumAsync(); + var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync(); + var canAccessPremium = await _stateService.CanAccessPremiumAsync(); if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault()) { var totpService = ServiceContainer.Resolve("totpService"); @@ -938,5 +942,16 @@ namespace Bit.Droid.Services Context.ClipboardService) as Android.Content.ClipboardManager; clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text); } + + public float GetSystemFontSizeScale() + { + var activity = CrossCurrentActivity.Current?.Activity as MainActivity; + return activity?.Resources?.Configuration?.FontScale ?? 1; + } + + public async Task OnAccountSwitchCompleteAsync() + { + // for any Android-specific cleanup required after switching accounts + } } } diff --git a/src/Android/Tiles/AutofillTileService.cs b/src/Android/Tiles/AutofillTileService.cs index 305afce37..7f9af7d8b 100644 --- a/src/Android/Tiles/AutofillTileService.cs +++ b/src/Android/Tiles/AutofillTileService.cs @@ -4,7 +4,6 @@ using Android.Content; using Android.Runtime; using Android.Service.QuickSettings; using Bit.App.Resources; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Bit.Droid.Accessibility; @@ -18,7 +17,7 @@ namespace Bit.Droid.Tile [Register("com.x8bit.bitwarden.AutofillTileService")] public class AutofillTileService : TileService { - private IStorageService _storageService; + private IStateService _stateService; public override void OnTileAdded() { @@ -59,11 +58,11 @@ namespace Bit.Droid.Tile private void SetTileAdded(bool isAdded) { AccessibilityHelpers.IsAutofillTileAdded = isAdded; - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - _storageService.SaveAsync(Constants.AutofillTileAdded, isAdded); + _stateService.SetAutofillTileAddedAsync(isAdded); } private void ScanAndFill() diff --git a/src/Android/Utilities/AppCenterHelper.cs b/src/Android/Utilities/AppCenterHelper.cs deleted file mode 100644 index d580fbbc6..000000000 --- a/src/Android/Utilities/AppCenterHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -#if !FDROID -using Bit.Core.Abstractions; -using System.Threading.Tasks; -using Microsoft.AppCenter; -using Microsoft.AppCenter.Crashes; -using Newtonsoft.Json; - -namespace Bit.Droid.Utilities -{ - public class AppCenterHelper - { - private const string AppSecret = "d3834185-b4a6-4347-9047-b86c65293d42"; - - private readonly IAppIdService _appIdService; - private readonly IUserService _userService; - - private string _userId; - private string _appId; - - public AppCenterHelper( - IAppIdService appIdService, - IUserService userService) - { - _appIdService = appIdService; - _userService = userService; - } - - public async Task InitAsync() - { - _userId = await _userService.GetUserIdAsync(); - _appId = await _appIdService.GetAppIdAsync(); - - AppCenter.Start(AppSecret, typeof(Crashes)); - AppCenter.SetUserId(_userId); - - Crashes.GetErrorAttachments = (ErrorReport report) => - { - return new ErrorAttachmentLog[] - { - ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"), - }; - }; - } - - public string Description - { - get - { - return JsonConvert.SerializeObject(new - { - AppId = _appId, - UserId = _userId - }, Formatting.Indented); - } - } - } -} -#endif diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index e9d2b7dfb..28eb7b712 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.View; -using System.Threading.Tasks; namespace Bit.App.Abstractions { @@ -35,6 +35,7 @@ namespace Bit.App.Abstractions void Background(); bool AutofillAccessibilityServiceRunning(); bool AutofillAccessibilityOverlayPermitted(); + bool HasAutofillService(); bool AutofillServiceEnabled(); void DisableAutofillService(); bool AutofillServicesEnabled(); @@ -45,5 +46,7 @@ namespace Bit.App.Abstractions long GetActiveTime(); void CloseMainApp(); bool SupportsFido2(); + float GetSystemFontSizeScale(); + Task OnAccountSwitchCompleteAsync(); } } diff --git a/src/App/Abstractions/IPasswordRepromptService.cs b/src/App/Abstractions/IPasswordRepromptService.cs index 6acd0ddd2..579d9ab44 100644 --- a/src/App/Abstractions/IPasswordRepromptService.cs +++ b/src/App/Abstractions/IPasswordRepromptService.cs @@ -7,7 +7,7 @@ namespace Bit.App.Abstractions string[] ProtectedFields { get; } Task ShowPasswordPromptAsync(); - + Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync(); Task Enabled(); diff --git a/src/App/Abstractions/IPushNotificationListenerService.cs b/src/App/Abstractions/IPushNotificationListenerService.cs index 65104a1c9..4a57c75a5 100644 --- a/src/App/Abstractions/IPushNotificationListenerService.cs +++ b/src/App/Abstractions/IPushNotificationListenerService.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace Bit.App.Abstractions { diff --git a/src/App/App.csproj b/src/App/App.csproj index 3f33e9ebb..62aff81cc 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -13,14 +13,14 @@ - - + + + - + - @@ -121,18 +121,21 @@ SendGroupingsPage.xaml Code + - + + MSBuild:UpdateDesignTimeXaml + @@ -417,5 +420,6 @@ + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 44c6531f8..bf6736836 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -1,15 +1,15 @@ -using Bit.App.Abstractions; +using System; +using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Pages; using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; -using Bit.Core; using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Utilities; -using System; -using System.Threading; -using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -18,7 +18,6 @@ namespace Bit.App { public partial class App : Application { - private readonly IUserService _userService; private readonly IBroadcasterService _broadcasterService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; @@ -26,7 +25,6 @@ namespace Bit.App private readonly ISyncService _syncService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuthService _authService; - private readonly IStorageService _storageService; private readonly IStorageService _secureStorageService; private readonly IDeviceActionService _deviceActionService; @@ -40,7 +38,6 @@ namespace Bit.App Current = this; return; } - _userService = ServiceContainer.Resolve("userService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); @@ -48,7 +45,6 @@ namespace Bit.App _syncService = ServiceContainer.Resolve("syncService"); _authService = ServiceContainer.Resolve("authService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _storageService = ServiceContainer.Resolve("storageService"); _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); @@ -77,7 +73,10 @@ namespace Bit.App } else if (message.Command == "locked") { - await LockedAsync(!(message.Data as bool?).GetValueOrDefault()); + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2 ?? false; + Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated)); } else if (message.Command == "lockVault") { @@ -85,8 +84,11 @@ namespace Bit.App } else if (message.Command == "logout") { - Device.BeginInvokeOnMainThread(async () => - await LogOutAsync((message.Data as bool?).GetValueOrDefault())); + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2 ?? true; + var expired = extras?.Item3 ?? false; + Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired)); } else if (message.Command == "loggedOut") { @@ -107,6 +109,18 @@ namespace Bit.App await SleptAsync(); } } + else if (message.Command == "addAccount") + { + await AddAccount(); + } + else if (message.Command == "accountAdded") + { + await UpdateThemeAsync(); + } + else if (message.Command == "switchedAccount") + { + await SwitchedAccountAsync(); + } else if (message.Command == "migrated") { await Task.Delay(1000); @@ -168,7 +182,7 @@ namespace Bit.App if (string.IsNullOrWhiteSpace(Options.Uri)) { var updated = await AppHelpers.PerformUpdateTasksAsync(_syncService, _deviceActionService, - _storageService); + _stateService); if (!updated) { SyncIfNeeded(); @@ -192,9 +206,12 @@ namespace Bit.App var isLocked = await _vaultTimeoutService.IsLockedAsync(); if (!isLocked) { - await _storageService.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); + await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); + } + if (!SetTabsPageFromAutofill(isLocked)) + { + ClearAutofillUri(); } - SetTabsPageFromAutofill(isLocked); await SleptAsync(); } } @@ -233,7 +250,7 @@ namespace Bit.App { await Device.InvokeOnMainThreadAsync(() => { - ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); + ThemeManager.SetTheme(Current.Resources); _messagingService.Send("updatedTheme"); }); } @@ -246,12 +263,12 @@ namespace Bit.App new System.Globalization.UmAlQuraCalendar(); } - private async Task LogOutAsync(bool expired) + private async Task LogOutAsync(string userId, bool userInitiated, bool expired) { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(userId, userInitiated); + await SetMainPageAsync(); _authService.LogOut(() => { - Current.MainPage = new HomePage(); if (expired) { _platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired); @@ -259,12 +276,50 @@ namespace Bit.App }); } + private async Task AddAccount() + { + Device.BeginInvokeOnMainThread(async () => + { + Options.HideAccountSwitcher = false; + Current.MainPage = new NavigationPage(new HomePage(Options)); + }); + } + + private async Task SwitchedAccountAsync() + { + await AppHelpers.OnAccountSwitchAsync(); + Device.BeginInvokeOnMainThread(async () => + { + if (await _vaultTimeoutService.ShouldTimeoutAsync()) + { + await _vaultTimeoutService.ExecuteTimeoutActionAsync(); + } + else + { + await SetMainPageAsync(); + } + await Task.Delay(50); + await UpdateThemeAsync(); + }); + } + private async Task SetMainPageAsync() { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (authed) { - if (await _vaultTimeoutService.IsLockedAsync()) + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + Current.MainPage = new NavigationPage(new LoginPage(email, Options)); + } + else if (await _vaultTimeoutService.IsLockedAsync() || + await _vaultTimeoutService.ShouldLockAsync()) { Current.MainPage = new NavigationPage(new LockPage(Options)); } @@ -287,20 +342,41 @@ namespace Bit.App } else { - Current.MainPage = new HomePage(Options); + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + Current.MainPage = new NavigationPage(new LoginPage(email, Options)); + } + else + { + Current.MainPage = new NavigationPage(new HomePage(Options)); + } } } private async Task ClearCacheIfNeededAsync() { - var lastClear = await _storageService.GetAsync(Constants.LastFileCacheClearKey); + var lastClear = await _stateService.GetLastFileCacheClearAsync(); if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1) { var task = Task.Run(() => _deviceActionService.ClearCacheAsync()); } } - private void SetTabsPageFromAutofill(bool isLocked) + private void ClearAutofillUri() + { + if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri)) + { + Options.Uri = null; + } + } + + private bool SetTabsPageFromAutofill(bool isLocked) { if (Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(Options.Uri) && !Options.FromAutofillFramework) @@ -320,7 +396,9 @@ namespace Bit.App } }); }); + return true; } + return false; } private void Prime() @@ -336,12 +414,12 @@ namespace Bit.App { InitializeComponent(); SetCulture(); - ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); + ThemeManager.SetTheme(Current.Resources); Current.RequestedThemeChanged += (s, a) => { UpdateThemeAsync(); }; - Current.MainPage = new HomePage(); + Current.MainPage = new NavigationPage(new HomePage(Options)); var mainPageTask = SetMainPageAsync(); ServiceContainer.Resolve("platformUtilsService").Init(); } @@ -363,12 +441,18 @@ namespace Bit.App }); } - private async Task LockedAsync(bool autoPromptBiometric) + private async Task LockedAsync(string userId, bool userInitiated) { - await _stateService.PurgeAsync(); + if (!await _stateService.IsActiveAccountAsync(userId)) + { + _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); + return; + } + + var autoPromptBiometric = !userInitiated; if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) { - var vaultTimeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); if (vaultTimeout == 0) { autoPromptBiometric = false; @@ -398,7 +482,7 @@ namespace Bit.App } } } - await _storageService.SaveAsync(Constants.PreviousPageKey, lastPageBeforeLock); + await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock); var lockPage = new LockPage(Options, autoPromptBiometric); Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage)); } diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml new file mode 100644 index 000000000..f0c52d0d4 --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs new file mode 100644 index 000000000..8096f9e19 --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -0,0 +1,188 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public partial class AccountSwitchingOverlayView : ContentView + { + public static readonly BindableProperty MainPageProperty = BindableProperty.Create( + nameof(MainPage), + typeof(ContentPage), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + + public static readonly BindableProperty MainFabProperty = BindableProperty.Create( + nameof(MainFab), + typeof(View), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + + public ContentPage MainPage + { + get => (ContentPage)GetValue(MainPageProperty); + set => SetValue(MainPageProperty, value); + } + + public View MainFab + { + get => (View)GetValue(MainFabProperty); + set => SetValue(MainFabProperty, value); + } + + readonly LazyResolve _logger = new LazyResolve("logger"); + + public AccountSwitchingOverlayView() + { + InitializeComponent(); + + ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand(LongPressAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + } + + public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel; + + public ICommand ToggleVisibililtyCommand { get; } + + public ICommand SelectAccountCommand { get; } + + public ICommand LongPressAccountCommand { get; } + + public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70; + + public async Task ToggleVisibilityAsync() + { + if (IsVisible) + { + await HideAsync(); + } + else + { + await ShowAsync(); + } + } + + public async Task ShowAsync() + { + if (ViewModel == null) + { + return; + } + + await ViewModel.RefreshAccountViewsAsync(); + + await Device.InvokeOnMainThreadAsync(async () => + { + // start listView in default (off-screen) position + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0); + + // re-measure in case accounts have been removed without changing screens + if (ViewModel.AccountViews != null) + { + _accountListView.HeightRequest = AccountListRowHeight * ViewModel.AccountViews.Count; + } + + // set overlay opacity to zero before making visible and start fade-in + Opacity = 0; + IsVisible = true; + this.FadeTo(1, 100); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-out + MainFab.FadeTo(0, 200); + } + + // slide account list into view + await _accountListContainer.TranslateTo(0, 0, 200, Easing.SinOut); + }); + } + + public async Task HideAsync() + { + if (!IsVisible) + { + // already hidden, don't animate again + return; + } + // Not all animations are awaited. This is intentional to allow multiple simultaneous animations. + await Device.InvokeOnMainThreadAsync(async () => + { + // start overlay fade-out + this.FadeTo(0, 200); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-in + MainFab.FadeTo(1, 200); + } + + // slide account list out of view + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 200, Easing.SinIn); + + // remove overlay + IsVisible = false; + }); + } + + private async void FreeSpaceOverlay_Tapped(object sender, EventArgs e) + { + try + { + await HideAsync(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + + private async Task SelectAccountAsync(AccountViewCellViewModel item) + { + try + { + await Task.Delay(100); + await HideAsync(); + + ViewModel?.SelectAccountCommand?.Execute(item); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + + private async Task LongPressAccountAsync(AccountViewCellViewModel item) + { + if (!item.IsAccount) + { + return; + } + try + { + await Task.Delay(100); + await HideAsync(); + + ViewModel?.LongPressAccountCommand?.Execute( + new Tuple(MainPage, item)); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + } +} diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs new file mode 100644 index 000000000..3f789b4d1 --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class AccountSwitchingOverlayViewModel : ExtendedViewModel + { + private readonly IStateService _stateService; + private readonly IMessagingService _messagingService; + + public AccountSwitchingOverlayViewModel(IStateService stateService, + IMessagingService messagingService, + ILogger logger) + { + _stateService = stateService; + _messagingService = messagingService; + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, + onException: ex => logger.Exception(ex), + allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand>(LongPressAccountAsync, + onException: ex => logger.Exception(ex), + allowsMultipleExecutions: false); + } + + // this needs to be a new list every time for the binding to get updated, + // XF doesn't currentlyl provide a direct way to update on same instance + // https://github.com/xamarin/Xamarin.Forms/issues/1950 + public List AccountViews => _stateService?.AccountViews is null ? null : new List(_stateService.AccountViews); + + public bool AllowActiveAccountSelection { get; set; } + + public bool AllowAddAccountRow { get; set; } + + public ICommand SelectAccountCommand { get; } + + public ICommand LongPressAccountCommand { get; } + + private async Task SelectAccountAsync(AccountViewCellViewModel item) + { + if (item.AccountView.IsAccount) + { + if (!item.AccountView.IsActive) + { + await _stateService.SetActiveUserAsync(item.AccountView.UserId); + _messagingService.Send("switchedAccount"); + } + else if (AllowActiveAccountSelection) + { + _messagingService.Send("switchedAccount"); + } + } + else + { + _messagingService.Send("addAccount"); + } + } + + private async Task LongPressAccountAsync(Tuple item) + { + var (page, account) = item; + if (account.AccountView.IsAccount) + { + await AppHelpers.AccountListOptions(page, account); + } + } + + public async Task RefreshAccountViewsAsync() + { + await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow); + + Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(AccountViews))); + } + } +} diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml b/src/App/Controls/AccountViewCell/AccountViewCell.xaml new file mode 100644 index 000000000..3d45cf929 --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs b/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs new file mode 100644 index 000000000..fa38816a5 --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs @@ -0,0 +1,54 @@ +using System.Windows.Input; +using Bit.Core.Models.View; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public partial class AccountViewCell : ViewCell + { + public static readonly BindableProperty AccountProperty = BindableProperty.Create( + nameof(Account), typeof(AccountView), typeof(AccountViewCell)); + + public static readonly BindableProperty SelectAccountCommandProperty = BindableProperty.Create( + nameof(SelectAccountCommand), typeof(ICommand), typeof(AccountViewCell)); + + public static readonly BindableProperty LongPressAccountCommandProperty = BindableProperty.Create( + nameof(LongPressAccountCommand), typeof(ICommand), typeof(AccountViewCell)); + + public AccountViewCell() + { + InitializeComponent(); + } + + public AccountView Account + { + get => GetValue(AccountProperty) as AccountView; + set => SetValue(AccountProperty, value); + } + + public ICommand SelectAccountCommand + { + get => GetValue(SelectAccountCommandProperty) as ICommand; + set => SetValue(SelectAccountCommandProperty, value); + } + + public ICommand LongPressAccountCommand + { + get => GetValue(LongPressAccountCommandProperty) as ICommand; + set => SetValue(LongPressAccountCommandProperty, value); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == AccountProperty.PropertyName) + { + if (Account == null) + { + return; + } + BindingContext = new AccountViewCellViewModel(Account); + } + } + } +} diff --git a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs new file mode 100644 index 000000000..3c2da33f2 --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs @@ -0,0 +1,93 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class AccountViewCellViewModel : ExtendedViewModel + { + private AccountView _accountView; + private AvatarImageSource _avatar; + + public AccountViewCellViewModel(AccountView accountView) + { + AccountView = accountView; + AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email); + } + + public AccountView AccountView + { + get => _accountView; + set => SetProperty(ref _accountView, value); + } + + public AvatarImageSource AvatarImageSource + { + get => _avatar; + set => SetProperty(ref _avatar, value); + } + + public bool IsAccount + { + get => AccountView.IsAccount; + } + + public bool ShowHostname + { + get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com"; + } + + public bool IsActive + { + get => AccountView.IsActive; + } + + public bool IsUnlocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Unlocked; + } + + public bool IsUnlockedAndNotActive + { + get => IsUnlocked && !IsActive; + } + + public bool IsLocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Locked; + } + + public bool IsLockedAndNotActive + { + get => IsLocked && !IsActive; + } + + public bool IsLoggedOut + { + get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut; + } + + public bool IsLoggedOutAndNotActive + { + get => IsLoggedOut && !IsActive; + } + + public string AuthStatusIconActive + { + get => BitwardenIcons.CheckCircle; + } + + public string AuthStatusIconNotActive + { + get + { + if (IsUnlocked) + { + return BitwardenIcons.Unlock; + } + return BitwardenIcons.Lock; + } + } + } +} diff --git a/src/App/Controls/AvatarImageSource.cs b/src/App/Controls/AvatarImageSource.cs new file mode 100644 index 000000000..fd9a86308 --- /dev/null +++ b/src/App/Controls/AvatarImageSource.cs @@ -0,0 +1,153 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SkiaSharp; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class AvatarImageSource : StreamImageSource + { + private string _data; + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (obj is AvatarImageSource avatar) + { + return avatar._data == _data; + } + + return base.Equals(obj); + } + + public override int GetHashCode() => _data?.GetHashCode() ?? -1; + + public AvatarImageSource(string name = null, string email = null) + { + _data = name; + if (string.IsNullOrWhiteSpace(_data)) + { + _data = email; + } + } + + public override Func> Stream => GetStreamAsync; + + private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) + { + OnLoadingStarted(); + userToken.Register(CancellationTokenSource.Cancel); + var result = Draw(); + OnLoadingCompleted(CancellationTokenSource.IsCancellationRequested); + return Task.FromResult(result); + } + + private Stream Draw() + { + string chars = null; + string upperData = null; + + if (string.IsNullOrEmpty(_data)) + { + chars = ".."; + } + else if (_data?.Length > 1) + { + upperData = _data.ToUpper(); + chars = GetFirstLetters(upperData, 2); + } + else + { + chars = upperData = _data.ToUpper(); + } + + var bgColor = StringToColor(upperData); + var textColor = Color.White; + var size = 50; + + var bitmap = new SKBitmap( + size * 2, + size * 2, + SKImageInfo.PlatformColorType, + SKAlphaType.Premul); + var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + + var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; + var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; + var radius = midX - midX / 5; + + var circlePaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor.ToHex()) + }; + canvas.DrawCircle(midX, midY, radius, circlePaint); + + var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); + var textSize = midX / 1.3f; + var textPaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + Color = SKColor.Parse(textColor.ToHex()), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }; + var rect = new SKRect(); + textPaint.MeasureText(chars, ref rect); + canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); + + return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream(); + } + + private string GetFirstLetters(string data, int charCount) + { + var parts = data.Split(); + if (parts.Length > 1 && charCount <= 2) + { + var text = ""; + for (int i = 0; i < charCount; i++) + { + text += parts[i].Substring(0, 1); + } + return text; + } + if (data.Length > 2) + { + return data.Substring(0, 2); + } + return data; + } + + private Color StringToColor(string str) + { + if (str == null) + { + return Color.FromHex("#33ffffff"); + } + var hash = 0; + for (var i = 0; i < str.Length; i++) + { + hash = str[i] + ((hash << 5) - hash); + } + var color = "#FF"; + for (var i = 0; i < 3; i++) + { + var value = (hash >> (i * 8)) & 0xff; + var base16 = "00" + Convert.ToString(value, 16); + color += base16.Substring(base16.Length - 2); + } + return Color.FromHex(color); + } + } +} diff --git a/src/App/Controls/CipherViewCell/CipherViewCell.xaml b/src/App/Controls/CipherViewCell/CipherViewCell.xaml index 9f2d6d2f4..5024ad537 100644 --- a/src/App/Controls/CipherViewCell/CipherViewCell.xaml +++ b/src/App/Controls/CipherViewCell/CipherViewCell.xaml @@ -12,10 +12,10 @@ x:DataType="controls:CipherViewCellViewModel"> - - - - + + + + @@ -23,7 +23,7 @@ - + @@ -35,17 +35,21 @@ StyleClass="list-icon, list-icon-platform" IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}" Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}" + ShouldUpdateFontSizeDynamicallyForAccesibility="True" AutomationProperties.IsInAccessibleTree="False" /> @@ -104,7 +108,7 @@ - + \ No newline at end of file diff --git a/src/App/Controls/CipherViewCell/CipherViewCell.xaml.cs b/src/App/Controls/CipherViewCell/CipherViewCell.xaml.cs index 54b87c1e7..f454f2b55 100644 --- a/src/App/Controls/CipherViewCell/CipherViewCell.xaml.cs +++ b/src/App/Controls/CipherViewCell/CipherViewCell.xaml.cs @@ -1,11 +1,16 @@ using System; +using Bit.App.Abstractions; using Bit.Core.Models.View; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Controls { public partial class CipherViewCell : ExtendedGrid { + private const int ICON_COLUMN_DEFAULT_WIDTH = 40; + private const int ICON_IMAGE_DEFAULT_WIDTH = 22; + public static readonly BindableProperty CipherProperty = BindableProperty.Create( nameof(Cipher), typeof(CipherView), typeof(CipherViewCell), default(CipherView), BindingMode.OneWay); @@ -18,6 +23,11 @@ namespace Bit.App.Controls public CipherViewCell() { InitializeComponent(); + + var fontScale = ServiceContainer.Resolve("deviceActionService").GetSystemFontSizeScale(); + _iconColumn.Width = new GridLength(ICON_COLUMN_DEFAULT_WIDTH * fontScale, GridUnitType.Absolute); + _iconImage.WidthRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; + _iconImage.HeightRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; } public bool? WebsiteIconsEnabled diff --git a/src/App/Controls/ExtendedStepper.cs b/src/App/Controls/ExtendedStepper.cs index 68191ae20..88103eed5 100644 --- a/src/App/Controls/ExtendedStepper.cs +++ b/src/App/Controls/ExtendedStepper.cs @@ -6,7 +6,7 @@ namespace Bit.App.Controls { public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create( nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default); - + public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create( nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default); @@ -15,7 +15,7 @@ namespace Bit.App.Controls get => (Color)GetValue(StepperBackgroundColorProperty); set => SetValue(StepperBackgroundColorProperty, value); } - + public Color StepperForegroundColor { get => (Color)GetValue(StepperForegroundColorProperty); diff --git a/src/App/Controls/ExtendedToolbarItem.cs b/src/App/Controls/ExtendedToolbarItem.cs new file mode 100644 index 000000000..9c6efe59a --- /dev/null +++ b/src/App/Controls/ExtendedToolbarItem.cs @@ -0,0 +1,29 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class ExtendedToolbarItem : ToolbarItem + { + public bool UseOriginalImage { get; set; } + + // HACK: For the issue of correctly updating the avatar toolbar item color on iOS + // we need to subscribe to the PropertyChanged event of the ToolbarItem on the CustomNavigationRenderer + // The problem is that there are a lot of private places where the navigation renderer disposes objects + // that we don't have access to, and that we should in order to properly prevent memory leaks + // So as a hack solution we have this OnAppearing/OnDisappearing actions and methods to be called on page lifecycle + // to subscribe/unsubscribe indirectly on the CustomNavigationRenderer + public Action OnAppearingAction { get; set; } + public Action OnDisappearingAction { get; set; } + + public void OnAppearing() + { + OnAppearingAction?.Invoke(); + } + + public void OnDisappearing() + { + OnDisappearingAction?.Invoke(); + } + } +} diff --git a/src/App/Controls/IconLabel.cs b/src/App/Controls/IconLabel.cs index 79310f936..fce9159f6 100644 --- a/src/App/Controls/IconLabel.cs +++ b/src/App/Controls/IconLabel.cs @@ -4,6 +4,8 @@ namespace Bit.App.Controls { public class IconLabel : Label { + public bool ShouldUpdateFontSizeDynamicallyForAccesibility { get; set; } + public IconLabel() { switch (Device.RuntimePlatform) diff --git a/src/App/Controls/SendViewCell/SendViewCell.xaml b/src/App/Controls/SendViewCell/SendViewCell.xaml index 2db1a40ce..8a9d0e2a6 100644 --- a/src/App/Controls/SendViewCell/SendViewCell.xaml +++ b/src/App/Controls/SendViewCell/SendViewCell.xaml @@ -19,7 +19,7 @@ - + @@ -31,6 +31,7 @@ VerticalOptions="Center" StyleClass="list-icon, list-icon-platform" Text="{Binding Send, Converter={StaticResource sendIconGlyphConverter}}" + ShouldUpdateFontSizeDynamicallyForAccesibility="True" AutomationProperties.IsInAccessibleTree="False" /> @@ -121,7 +122,7 @@ ), typeof(SendViewCell)); - + public static readonly BindableProperty ShowOptionsProperty = BindableProperty.Create( nameof(ShowOptions), typeof(bool), typeof(SendViewCell), true, BindingMode.OneWay); public SendViewCell() { InitializeComponent(); + + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _iconColumn.Width = new GridLength(40 * deviceActionService.GetSystemFontSizeScale(), GridUnitType.Absolute); } public SendView Send diff --git a/src/App/Effects/FabShadowEffect.cs b/src/App/Effects/FabShadowEffect.cs index 3e75d85d9..1ee706891 100644 --- a/src/App/Effects/FabShadowEffect.cs +++ b/src/App/Effects/FabShadowEffect.cs @@ -4,8 +4,8 @@ namespace Bit.App.Effects { public class FabShadowEffect : RoutingEffect { - public FabShadowEffect() - : base("Bitwarden.FabShadowEffect") + public FabShadowEffect() + : base("Bitwarden.FabShadowEffect") { } } } diff --git a/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs b/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs new file mode 100644 index 000000000..c80199c84 --- /dev/null +++ b/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs @@ -0,0 +1,33 @@ +using Xamarin.Forms; + +namespace Bit.App.Effects +{ + public enum ScrollContentInsetAdjustmentBehavior + { + Automatic, + ScrollableAxes, + Never, + Always + } + + public class ScrollViewContentInsetAdjustmentBehaviorEffect : RoutingEffect + { + public static readonly BindableProperty ContentInsetAdjustmentBehaviorProperty = + BindableProperty.CreateAttached("ContentInsetAdjustmentBehavior", typeof(ScrollContentInsetAdjustmentBehavior), typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), ScrollContentInsetAdjustmentBehavior.Automatic); + + public static ScrollContentInsetAdjustmentBehavior GetContentInsetAdjustmentBehavior(BindableObject view) + { + return (ScrollContentInsetAdjustmentBehavior)view.GetValue(ContentInsetAdjustmentBehaviorProperty); + } + + public static void SetContentInsetAdjustmentBehavior(BindableObject view, ScrollContentInsetAdjustmentBehavior value) + { + view.SetValue(ContentInsetAdjustmentBehaviorProperty, value); + } + + public ScrollViewContentInsetAdjustmentBehaviorEffect() + : base($"Bitwarden.{nameof(ScrollViewContentInsetAdjustmentBehaviorEffect)}") + { + } + } +} diff --git a/src/App/Models/AppOptions.cs b/src/App/Models/AppOptions.cs index 7d7e2b22c..0a251d7cd 100644 --- a/src/App/Models/AppOptions.cs +++ b/src/App/Models/AppOptions.cs @@ -22,6 +22,7 @@ namespace Bit.App.Models public bool IosExtension { get; set; } public Tuple CreateSend { get; set; } public bool CopyInsteadOfShareAfterSaving { get; set; } + public bool HideAccountSwitcher { get; set; } public void SetAllFrom(AppOptions o) { @@ -46,6 +47,7 @@ namespace Bit.App.Models IosExtension = o.IosExtension; CreateSend = o.CreateSend; CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving; + HideAccountSwitcher = o.HideAccountSwitcher; } } } diff --git a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs index b8ed2a230..81d16568e 100644 --- a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs +++ b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs @@ -1,12 +1,12 @@ -using Bit.App.Resources; -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; using Bit.Core.Models.Domain; +using Bit.Core.Utilities; using Xamarin.Essentials; namespace Bit.App.Pages @@ -14,7 +14,7 @@ namespace Bit.App.Pages public class BaseChangePasswordViewModel : BaseViewModel { protected readonly IPlatformUtilsService _platformUtilsService; - protected readonly IUserService _userService; + protected readonly IStateService _stateService; protected readonly IPolicyService _policyService; protected readonly IPasswordGenerationService _passwordGenerationService; protected readonly II18nService _i18nService; @@ -31,7 +31,7 @@ namespace Bit.App.Pages protected BaseChangePasswordViewModel() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _policyService = ServiceContainer.Resolve("policyService"); _passwordGenerationService = ServiceContainer.Resolve("passwordGenerationService"); @@ -46,7 +46,11 @@ namespace Bit.App.Pages { get => _showPassword; set => SetProperty(ref _showPassword, value, - additionalPropertyNames: new[] { nameof(ShowPasswordIcon) }); + additionalPropertyNames: new[] + { + nameof(ShowPasswordIcon), + nameof(PasswordVisibilityAccessibilityText) + }); } public bool IsPolicyInEffect @@ -66,8 +70,9 @@ namespace Bit.App.Pages get => _policy; set => SetProperty(ref _policy, value); } - + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string MasterPassword { get; set; } public string ConfirmMasterPassword { get; set; } public string Hint { get; set; } @@ -84,7 +89,7 @@ namespace Bit.App.Pages await CheckPasswordPolicy(); } } - + private async Task CheckPasswordPolicy() { Policy = await _policyService.GetMasterPasswordPolicyOptions(); @@ -166,13 +171,13 @@ namespace Bit.App.Pages AppResources.AnErrorHasOccurred, AppResources.Ok); return false; } - + return true; } private async Task> GetPasswordStrengthUserInput() { - var email = await _userService.GetEmailAsync(); + var email = await _stateService.GetEmailAsync(); List userInput = null; var atPosition = email.IndexOf('@'); if (atPosition > -1) diff --git a/src/App/Pages/Accounts/DeleteAccountViewModel.cs b/src/App/Pages/Accounts/DeleteAccountViewModel.cs index cab70bd38..6705e1949 100644 --- a/src/App/Pages/Accounts/DeleteAccountViewModel.cs +++ b/src/App/Pages/Accounts/DeleteAccountViewModel.cs @@ -5,9 +5,6 @@ using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Utilities; -#if !FDROID -using Microsoft.AppCenter.Crashes; -#endif namespace Bit.App.Pages { @@ -15,11 +12,13 @@ namespace Bit.App.Pages { readonly IPlatformUtilsService _platformUtilsService; readonly IVerificationActionsFlowHelper _verificationActionsFlowHelper; + readonly ILogger _logger; public DeleteAccountViewModel() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _verificationActionsFlowHelper = ServiceContainer.Resolve("verificationActionsFlowHelper"); + _logger = ServiceContainer.Resolve("logger"); PageTitle = AppResources.DeleteAccount; } @@ -44,9 +43,7 @@ namespace Bit.App.Pages } catch (System.Exception ex) { -#if !FDROID - Crashes.TrackError(ex); -#endif + _logger.Exception(ex); await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred); } } @@ -60,16 +57,19 @@ namespace Bit.App.Pages readonly IMessagingService _messagingService; readonly IPlatformUtilsService _platformUtilsService; readonly IDeviceActionService _deviceActionService; + readonly ILogger _logger; public DeleteAccountActionFlowExecutioner(IApiService apiService, IMessagingService messagingService, IPlatformUtilsService platformUtilsService, - IDeviceActionService deviceActionService) + IDeviceActionService deviceActionService, + ILogger logger) { _apiService = apiService; _messagingService = messagingService; _platformUtilsService = platformUtilsService; _deviceActionService = deviceActionService; + _logger = logger; } public async Task Execute(IActionFlowParmeters parameters) @@ -102,9 +102,7 @@ namespace Bit.App.Pages catch (System.Exception ex) { await _deviceActionService.HideLoadingAsync(); -#if !FDROID - Crashes.TrackError(ex); -#endif + _logger.Exception(ex); await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred); } } diff --git a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs index d6a916131..b57d2f440 100644 --- a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs +++ b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs @@ -1,8 +1,8 @@ -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using System; +using System; using System.Threading.Tasks; using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages @@ -10,14 +10,11 @@ namespace Bit.App.Pages public partial class EnvironmentPage : BaseContentPage { private readonly IPlatformUtilsService _platformUtilsService; - private readonly IMessagingService _messagingService; private readonly EnvironmentPageViewModel _vm; public EnvironmentPage() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _messagingService.Send("showStatusBar", true); InitializeComponent(); _vm = BindingContext as EnvironmentPageViewModel; _vm.Page = this; @@ -35,7 +32,6 @@ namespace Bit.App.Pages _vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync()); _vm.CloseAction = async () => { - _messagingService.Send("showStatusBar", false); await Navigation.PopModalAsync(); }; } diff --git a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs index c47491668..b25113c41 100644 --- a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs +++ b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs @@ -1,8 +1,8 @@ -using Bit.App.Resources; +using System; +using System.Threading.Tasks; +using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Utilities; -using System; -using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages diff --git a/src/App/Pages/Accounts/HintPageViewModel.cs b/src/App/Pages/Accounts/HintPageViewModel.cs index cb316759f..b074012e8 100644 --- a/src/App/Pages/Accounts/HintPageViewModel.cs +++ b/src/App/Pages/Accounts/HintPageViewModel.cs @@ -1,9 +1,9 @@ -using Bit.App.Abstractions; +using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Utilities; -using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages diff --git a/src/App/Pages/Accounts/HomePage.xaml b/src/App/Pages/Accounts/HomePage.xaml index 84c727ba6..8276861e1 100644 --- a/src/App/Pages/Accounts/HomePage.xaml +++ b/src/App/Pages/Accounts/HomePage.xaml @@ -6,49 +6,76 @@ xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:u="clr-namespace:Bit.App.Utilities" - xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore" x:DataType="pages:HomeViewModel" + x:Name="_page" Title="{Binding PageTitle}"> - + - + + - - - - - - - - - - - - - - + - - - - - - - - - - - @@ -250,29 +253,33 @@ + Spacing="0" + xct:TouchEffect.Command="{Binding ToggleOptionsCommand}" + AutomationProperties.IsInAccessibleTree="True" + AutomationProperties.Name="{Binding OptionsAccessilibityText}"> + + diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs index 69d04cda5..aed80316f 100644 --- a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs +++ b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs @@ -1,13 +1,12 @@ -using Bit.App.Models; -using Bit.App.Resources; -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using Bit.Core.Utilities; -using System; +using System; using System.Linq; using System.Threading.Tasks; using Bit.App.Controls; +using Bit.App.Models; using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages @@ -15,7 +14,8 @@ namespace Bit.App.Pages public partial class AutofillCiphersPage : BaseContentPage { private readonly AppOptions _appOptions; - private readonly IPlatformUtilsService _platformUtilsService; + private readonly IBroadcasterService _broadcasterService; + private readonly ISyncService _syncService; private readonly IVaultTimeoutService _vaultTimeoutService; private AutofillCiphersPageViewModel _vm; @@ -24,17 +24,23 @@ namespace Bit.App.Pages { _appOptions = appOptions; InitializeComponent(); + SetActivityIndicator(_mainContent); _vm = BindingContext as AutofillCiphersPageViewModel; _vm.Page = this; _vm.Init(appOptions); - _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _syncService = ServiceContainer.Resolve("syncService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); } protected async override void OnAppearing() { base.OnAppearing(); + if (_syncService.SyncInProgress) + { + IsBusy = true; + } if (!await AppHelpers.IsVaultTimeoutImmediateAsync()) { await _vaultTimeoutService.CheckVaultTimeoutAsync(); @@ -43,13 +49,37 @@ namespace Bit.App.Pages { return; } + + _accountAvatar?.OnAppearing(); + _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); + + _broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) => + { + if (message.Command == "syncStarted") + { + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => + { + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + }); + await LoadOnAppearedAsync(_mainLayout, false, async () => { try { await _vm.LoadAsync(); } - catch (Exception e) when(e.Message.Contains("No key.")) + catch (Exception e) when (e.Message.Contains("No key.")) { await Task.Delay(1000); await _vm.LoadAsync(); @@ -59,6 +89,11 @@ namespace Bit.App.Pages protected override bool OnBackButtonPressed() { + if (_accountListOverlay.IsVisible) + { + _accountListOverlay.HideAsync().FireAndForget(); + return true; + } if (Device.RuntimePlatform == Device.Android) { _appOptions.Uri = null; @@ -66,6 +101,13 @@ namespace Bit.App.Pages return base.OnBackButtonPressed(); } + protected override void OnDisappearing() + { + base.OnDisappearing(); + IsBusy = false; + _accountAvatar?.OnDisappearing(); + } + private async void RowSelected(object sender, SelectionChangedEventArgs e) { ((ExtendedCollectionView)sender).SelectedItem = null; diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs index 6deb7710a..80e9caf09 100644 --- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -1,4 +1,8 @@ -using Bit.App.Abstractions; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Models; using Bit.App.Resources; using Bit.App.Utilities; @@ -8,11 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Bit.App.Pages @@ -24,8 +24,10 @@ namespace Bit.App.Pages private readonly ICipherService _cipherService; private readonly IStateService _stateService; private readonly IPasswordRepromptService _passwordRepromptService; + private readonly IMessagingService _messagingService; + private readonly ILogger _logger; - private AppOptions _appOptions; + private bool _showNoData; private bool _showList; private string _noDataText; private bool _websiteIconsEnabled; @@ -37,15 +39,30 @@ namespace Bit.App.Pages _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _stateService = ServiceContainer.Resolve("stateService"); _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _logger = ServiceContainer.Resolve("logger"); - GroupedItems = new ExtendedObservableCollection(); + GroupedItems = new ObservableRangeCollection(); CipherOptionsCommand = new Command(CipherOptionsAsync); + + AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) + { + AllowAddAccountRow = false + }; } public string Name { get; set; } public string Uri { get; set; } public Command CipherOptionsCommand { get; set; } - public ExtendedObservableCollection GroupedItems { get; set; } + public bool LoadedOnce { get; set; } + public ObservableRangeCollection GroupedItems { get; set; } + public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } + + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } public bool ShowList { @@ -66,10 +83,9 @@ namespace Bit.App.Pages public void Init(AppOptions appOptions) { - _appOptions = appOptions; - Uri = appOptions.Uri; + Uri = appOptions?.Uri; string name = null; - if (Uri.StartsWith(Constants.AndroidAppProtocol)) + if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false) { name = Uri.Substring(Constants.AndroidAppProtocol.Length); } @@ -88,9 +104,10 @@ namespace Bit.App.Pages public async Task LoadAsync() { - WebsiteIconsEnabled = !(await _stateService.GetAsync(Constants.DisableFaviconKey)) - .GetValueOrDefault(); + LoadedOnce = true; ShowList = false; + ShowNoData = false; + WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); var groupedItems = new List(); var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null); var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); @@ -108,8 +125,51 @@ namespace Bit.App.Pages new GroupingsPageListGroup(fuzzy, AppResources.PossibleMatchingItems, fuzzy.Count, false, !hasMatching)); } - GroupedItems.ResetWithRange(groupedItems); + + // TODO: refactor this + if (Device.RuntimePlatform == Device.Android + || + GroupedItems.Any()) + { + var items = new List(); + foreach (var itemGroup in groupedItems) + { + items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); + items.AddRange(itemGroup); + } + + GroupedItems.ReplaceRange(items); + } + else + { + // HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list + var first = true; + var items = new List(); + foreach (var itemGroup in groupedItems) + { + if (!first) + { + items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); + } + else + { + first = false; + } + items.AddRange(itemGroup); + } + + if (groupedItems.Any()) + { + GroupedItems.ReplaceRange(new List { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) }); + GroupedItems.AddRange(items); + } + else + { + GroupedItems.Clear(); + } + } ShowList = groupedItems.Any(); + ShowNoData = !ShowList; } public async Task SelectCipherAsync(CipherView cipher, bool fuzzy) diff --git a/src/App/Pages/Vault/CiphersPage.xaml b/src/App/Pages/Vault/CiphersPage.xaml index d9a31fa53..8b463e1a3 100644 --- a/src/App/Pages/Vault/CiphersPage.xaml +++ b/src/App/Pages/Vault/CiphersPage.xaml @@ -33,7 +33,9 @@ Text="" VerticalOptions="CenterAndExpand" Clicked="BackButton_Clicked" - x:Name="_backButton" /> + x:Name="_backButton" + AutomationProperties.IsInAccessibleTree="True" + AutomationProperties.Name="{u:I18n TapToGoBack}"/> filter, bool folder = false, bool collection = false, - bool type = false, string autofillUrl = null, bool deleted = false) + public CiphersPage(Func filter, string pageTitle = null, string autofillUrl = null, bool deleted = false) { InitializeComponent(); _vm = BindingContext as CiphersPageViewModel; @@ -26,21 +25,9 @@ namespace Bit.App.Pages _vm.Filter = filter; _vm.AutofillUrl = _autofillUrl = autofillUrl; _vm.Deleted = deleted; - if (deleted) + if (pageTitle != null) { - _vm.PageTitle = AppResources.SearchTrash; - } - else if (folder) - { - _vm.PageTitle = AppResources.SearchFolder; - } - else if (collection) - { - _vm.PageTitle = AppResources.SearchCollection; - } - else if (type) - { - _vm.PageTitle = AppResources.SearchType; + _vm.PageTitle = string.Format(AppResources.SearchGroup, pageTitle); } else { @@ -53,6 +40,7 @@ namespace Bit.App.Pages _searchBar.Placeholder = AppResources.Search; _mainLayout.Children.Insert(0, _searchBar); _mainLayout.Children.Insert(1, _separator); + ShowModalAnimationDelay = 0; } else { diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs index 4c11bd68c..d9a5510e2 100644 --- a/src/App/Pages/Vault/CiphersPageViewModel.cs +++ b/src/App/Pages/Vault/CiphersPageViewModel.cs @@ -1,4 +1,9 @@ -using Bit.App.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core; using Bit.Core.Abstractions; @@ -6,11 +11,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages @@ -76,8 +76,7 @@ namespace Bit.App.Pages public async Task InitAsync() { - WebsiteIconsEnabled = !(await _stateService.GetAsync(Constants.DisableFaviconKey)) - .GetValueOrDefault(); + WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text)) { Search((Page as CiphersPage).SearchBar.Text, 200); @@ -108,7 +107,7 @@ namespace Bit.App.Pages } try { - ciphers = await _searchService.SearchCiphersAsync(searchText, + ciphers = await _searchService.SearchCiphersAsync(searchText, Filter ?? (c => c.IsDeleted == Deleted), null, cts.Token); cts.Token.ThrowIfCancellationRequested(); } diff --git a/src/App/Pages/Vault/CollectionsPageViewModel.cs b/src/App/Pages/Vault/CollectionsPageViewModel.cs index e0b678de2..f58ab1a78 100644 --- a/src/App/Pages/Vault/CollectionsPageViewModel.cs +++ b/src/App/Pages/Vault/CollectionsPageViewModel.cs @@ -1,13 +1,13 @@ -using Bit.App.Abstractions; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Bit.App.Pages { diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml index cd9a3332a..7df644850 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml @@ -6,6 +6,7 @@ xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:effects="clr-namespace:Bit.App.Effects" xmlns:controls="clr-namespace:Bit.App.Controls" + xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore" x:DataType="pages:GroupingsPageViewModel" Title="{Binding PageTitle}" x:Name="_page"> @@ -15,6 +16,15 @@ + @@ -46,33 +56,81 @@ + StyleClass="list-row, list-row-platform"> + HorizontalOptions="Start" + VerticalOptions="Center" + StyleClass="list-icon, list-icon-platform" + ShouldUpdateFontSizeDynamicallyForAccesibility="True"> + + + + + + + + + + + + - - - - - - - - - - - - + StyleClass="list, list-platform" /> @@ -135,6 +170,8 @@ AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0, 0, 1, 1"> + + + + diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs index 13bf9f6ec..469e1bc59 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -1,14 +1,13 @@ -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Resources; -using Bit.Core; -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using Bit.Core.Utilities; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Controls; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages @@ -18,7 +17,7 @@ namespace Bit.App.Pages private readonly IBroadcasterService _broadcasterService; private readonly ISyncService _syncService; private readonly IPushNotificationService _pushNotificationService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IVaultTimeoutService _vaultTimeoutService; private readonly ICipherService _cipherService; private readonly IDeviceActionService _deviceActionService; @@ -28,8 +27,8 @@ namespace Bit.App.Pages private PreviousPageInfo _previousPage; public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null, - string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null, - bool deleted = false) + string collectionId = null, string pageTitle = null, string vaultFilterSelection = null, + PreviousPageInfo previousPage = null, bool deleted = false) { _pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks); InitializeComponent(); @@ -37,7 +36,7 @@ namespace Bit.App.Pages _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _syncService = ServiceContainer.Resolve("syncService"); _pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _cipherService = ServiceContainer.Resolve("cipherService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); @@ -53,6 +52,10 @@ namespace Bit.App.Pages { _vm.PageTitle = pageTitle; } + if (vaultFilterSelection != null) + { + _vm.VaultFilterDescription = vaultFilterSelection; + } if (Device.RuntimePlatform == Device.iOS) { @@ -70,6 +73,10 @@ namespace Bit.App.Pages _absLayout.Children.Remove(_fab); ToolbarItems.Remove(_addItem); } + if (!mainPage) + { + ToolbarItems.Remove(_accountAvatar); + } } protected async override void OnAppearing() @@ -80,6 +87,12 @@ namespace Bit.App.Pages IsBusy = true; } + _accountAvatar?.OnAppearing(); + if (_vm.MainPage) + { + _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); + } + _broadcasterService.Subscribe(_pageName, async (message) => { if (message.Command == "syncStarted") @@ -100,7 +113,6 @@ namespace Bit.App.Pages } }); - var migratedFromV1 = await _storageService.GetAsync(Constants.MigratedFromV1); await LoadOnAppearedAsync(_mainLayout, false, async () => { if (!_syncService.SyncInProgress || (await _cipherService.GetAllAsync()).Any()) @@ -109,7 +121,7 @@ namespace Bit.App.Pages { await _vm.LoadAsync(); } - catch (Exception e) when(e.Message.Contains("No key.")) + catch (Exception e) when (e.Message.Contains("No key.")) { await Task.Delay(1000); await _vm.LoadAsync(); @@ -123,18 +135,6 @@ namespace Bit.App.Pages await _vm.LoadAsync(); } } - // Forced sync if for some reason we have no data after a v1 migration - if (_vm.MainPage && !_syncService.SyncInProgress && migratedFromV1.GetValueOrDefault() && - !_vm.HasCiphers && - Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) - { - var triedV1ReSync = await _storageService.GetAsync(Constants.TriedV1Resync); - if (!triedV1ReSync.GetValueOrDefault()) - { - await _storageService.SaveAsync(Constants.TriedV1Resync, true); - await _syncService.FullSyncAsync(true); - } - } await ShowPreviousPageAsync(); AdjustToolbar(); }, _mainContent); @@ -145,14 +145,14 @@ namespace Bit.App.Pages } // Push registration - var lastPushRegistration = await _storageService.GetAsync(Constants.PushLastRegistrationDateKey); + var lastPushRegistration = await _stateService.GetPushLastRegistrationDateAsync(); lastPushRegistration = lastPushRegistration.GetValueOrDefault(DateTime.MinValue); if (Device.RuntimePlatform == Device.iOS) { - var pushPromptShow = await _storageService.GetAsync(Constants.PushInitialPromptShownKey); + var pushPromptShow = await _stateService.GetPushInitialPromptShownAsync(); if (!pushPromptShow.GetValueOrDefault(false)) { - await _storageService.SaveAsync(Constants.PushInitialPromptShownKey, true); + await _stateService.SetPushInitialPromptShownAsync(true); await DisplayAlert(AppResources.EnableAutomaticSyncing, AppResources.PushNotificationAlert, AppResources.OkGotIt); } @@ -168,30 +168,26 @@ namespace Bit.App.Pages { await _pushNotificationService.RegisterAsync(); } - if (!_deviceActionService.AutofillAccessibilityServiceRunning() - && !_deviceActionService.AutofillServiceEnabled()) - { - if (migratedFromV1.GetValueOrDefault()) - { - var migratedFromV1AutofillPromptShown = await _storageService.GetAsync( - Constants.MigratedFromV1AutofillPromptShown); - if (!migratedFromV1AutofillPromptShown.GetValueOrDefault()) - { - await DisplayAlert(AppResources.Autofill, - AppResources.AutofillServiceNotEnabled, AppResources.Ok); - } - } - } - await _storageService.SaveAsync(Constants.MigratedFromV1AutofillPromptShown, true); } } + protected override bool OnBackButtonPressed() + { + if (_accountListOverlay.IsVisible) + { + _accountListOverlay.HideAsync().FireAndForget(); + return true; + } + return false; + } + protected override void OnDisappearing() { base.OnDisappearing(); IsBusy = false; _broadcasterService.Unsubscribe(_pageName); _vm.DisableRefreshing(); + _accountAvatar?.OnDisappearing(); } private async void RowSelected(object sender, SelectionChangedEventArgs e) @@ -230,31 +226,41 @@ namespace Bit.App.Pages private async void Search_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); if (DoOnce()) { - var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null, - _vm.Type != null, deleted: _vm.Deleted); + var page = new CiphersPage(_vm.Filter, _vm.MainPage ? null : _vm.PageTitle, deleted: _vm.Deleted); await Navigation.PushModalAsync(new NavigationPage(page)); } } private async void Sync_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vm.SyncAsync(); } private async void Lock_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vaultTimeoutService.LockAsync(true, true); } private async void Exit_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vm.ExitAsync(); } private async void AddButton_Clicked(object sender, EventArgs e) { + var skipAction = _accountListOverlay.IsVisible && Device.RuntimePlatform == Device.Android; + await _accountListOverlay.HideAsync(); + if (skipAction) + { + // Account list in the process of closing via tapping on invisible FAB, skip this attempt + return; + } if (!_vm.Deleted && DoOnce()) { var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId); @@ -268,6 +274,7 @@ namespace Bit.App.Pages { return; } + await _accountListOverlay.HideAsync(); if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId)) { await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId))); @@ -284,5 +291,10 @@ namespace Bit.App.Pages _addItem.IsEnabled = !_vm.Deleted; _addItem.IconImageSource = _vm.Deleted ? null : "plus.png"; } + + public async Task HideAccountSwitchingOverlayAsync() + { + await _accountListOverlay.HideAsync(); + } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageHeaderListItem.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageHeaderListItem.cs new file mode 100644 index 000000000..e7912ebc2 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageHeaderListItem.cs @@ -0,0 +1,14 @@ +namespace Bit.App.Pages +{ + public class GroupingsPageHeaderListItem : IGroupingsPageListItem + { + public GroupingsPageHeaderListItem(string title, string itemCount) + { + Title = title; + ItemCount = itemCount; + } + + public string Title { get; } + public string ItemCount { get; set; } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs index 11e15677a..34674d961 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs @@ -5,7 +5,7 @@ using Bit.Core.Models.View; namespace Bit.App.Pages { - public class GroupingsPageListItem + public class GroupingsPageListItem : IGroupingsPageListItem { private string _icon; private string _name; diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs index 667aeee96..a2e2207b4 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs @@ -4,11 +4,17 @@ namespace Bit.App.Pages { public class GroupingsPageListItemSelector : DataTemplateSelector { + public DataTemplate HeaderTemplate { get; set; } public DataTemplate CipherTemplate { get; set; } public DataTemplate GroupTemplate { get; set; } protected override DataTemplate OnSelectTemplate(object item, BindableObject container) { + if (item is GroupingsPageHeaderListItem) + { + return HeaderTemplate; + } + if (item is GroupingsPageListItem listItem) { return listItem.Cipher != null ? CipherTemplate : GroupTemplate; diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index 228c94fb6..94027f5c3 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -1,16 +1,18 @@ -using Bit.App.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Resources; using Bit.App.Utilities; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Bit.App.Pages @@ -28,7 +30,10 @@ namespace Bit.App.Pages private bool _showList; private bool _websiteIconsEnabled; private bool _syncRefreshing; + private bool _showVaultFilter; + private string _vaultFilterSelection; private string _noDataText; + private List _organizations; private List _allCiphers; private Dictionary _folderCounts = new Dictionary(); private Dictionary _collectionCounts = new Dictionary(); @@ -39,14 +44,15 @@ namespace Bit.App.Pages private readonly IFolderService _folderService; private readonly ICollectionService _collectionService; private readonly ISyncService _syncService; - private readonly IUserService _userService; private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IDeviceActionService _deviceActionService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; - private readonly IStorageService _storageService; private readonly IPasswordRepromptService _passwordRepromptService; + private readonly IOrganizationService _organizationService; + private readonly IPolicyService _policyService; + private readonly ILogger _logger; public GroupingsPageViewModel() { @@ -54,24 +60,32 @@ namespace Bit.App.Pages _folderService = ServiceContainer.Resolve("folderService"); _collectionService = ServiceContainer.Resolve("collectionService"); _syncService = ServiceContainer.Resolve("syncService"); - _userService = ServiceContainer.Resolve("userService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); - _storageService = ServiceContainer.Resolve("storageService"); _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); + _organizationService = ServiceContainer.Resolve("organizationService"); + _policyService = ServiceContainer.Resolve("policyService"); + _logger = ServiceContainer.Resolve("logger"); Loading = true; - PageTitle = AppResources.MyVault; - GroupedItems = new ExtendedObservableCollection(); + GroupedItems = new ObservableRangeCollection(); RefreshCommand = new Command(async () => { Refreshing = true; await LoadAsync(); }); CipherOptionsCommand = new Command(CipherOptionsAsync); + VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync, + onException: ex => _logger.Exception(ex), + allowsMultipleExecutions: false); + + AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) + { + AllowAddAccountRow = true + }; } public bool MainPage { get; set; } @@ -80,12 +94,12 @@ namespace Bit.App.Pages public string CollectionId { get; set; } public Func Filter { get; set; } public bool Deleted { get; set; } - public bool HasCiphers { get; set; } public bool HasFolders { get; set; } public bool HasCollections { get; set; } - public bool ShowNoFolderCiphers => (NoFolderCiphers?.Count ?? int.MaxValue) < NoFolderListSize && - (!Collections?.Any() ?? true); + public bool ShowNoFolderCipherGroup => NoFolderCiphers != null + && NoFolderCiphers.Count < NoFolderListSize + && (Collections is null || !Collections.Any()); public List Ciphers { get; set; } public List FavoriteCiphers { get; set; } public List NoFolderCiphers { get; set; } @@ -139,9 +153,30 @@ namespace Bit.App.Pages get => _websiteIconsEnabled; set => SetProperty(ref _websiteIconsEnabled, value); } - public ExtendedObservableCollection GroupedItems { get; set; } + public bool ShowVaultFilter + { + get => _showVaultFilter; + set => SetProperty(ref _showVaultFilter, value); + } + public string VaultFilterDescription + { + get + { + if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults) + { + return string.Format(AppResources.VaultFilterDescription, AppResources.All); + } + return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection); + } + set => SetProperty(ref _vaultFilterSelection, value); + } + + public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } + + public ObservableRangeCollection GroupedItems { get; set; } public Command RefreshCommand { get; set; } public Command CipherOptionsCommand { get; set; } + public ICommand VaultFilterCommand { get; } public bool LoadedOnce { get; set; } public async Task LoadAsync() @@ -150,7 +185,7 @@ namespace Bit.App.Pages { return; } - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; @@ -159,13 +194,24 @@ namespace Bit.App.Pages { return; } - if (await _storageService.GetAsync(Constants.SyncOnRefreshKey) && Refreshing && !SyncRefreshing) + if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing) { SyncRefreshing = true; await _syncService.FullSyncAsync(false); return; } + _organizations = await _organizationService.GetAllAsync(); + if (MainPage) + { + ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync(); + if (ShowVaultFilter && _vaultFilterSelection == null) + { + _vaultFilterSelection = AppResources.AllVaults; + } + PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault; + } + _doingLoad = true; LoadedOnce = true; ShowNoData = false; @@ -175,14 +221,13 @@ namespace Bit.App.Pages var groupedItems = new List(); var page = Page as GroupingsPage; - WebsiteIconsEnabled = !(await _stateService.GetAsync(Constants.DisableFaviconKey)) - .GetValueOrDefault(); + WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); try { await LoadDataAsync(); - if (ShowNoFolderCiphers && (NestedFolders?.Any() ?? false)) + if (ShowNoFolderCipherGroup && (NestedFolders?.Any() ?? false)) { - // Remove "No Folder" from folder listing + // Remove "No Folder" folder from folders group NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1); } @@ -257,7 +302,7 @@ namespace Bit.App.Pages groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); } - if (ShowNoFolderCiphers) + if (ShowNoFolderCipherGroup) { var noFolderCiphersListItems = NoFolderCiphers.Select( c => new GroupingsPageListItem { Cipher = c }).ToList(); @@ -271,12 +316,66 @@ namespace Bit.App.Pages { new GroupingsPageListItem() { - IsTrash = true, + IsTrash = true, ItemCount = _deletedCount.ToString("N0") } }, AppResources.Trash, _deletedCount, uppercaseGroupNames, false)); } - GroupedItems.ResetWithRange(groupedItems); + + // TODO: refactor this + if (Device.RuntimePlatform == Device.Android + || + GroupedItems.Any()) + { + var items = new List(); + foreach (var itemGroup in groupedItems) + { + items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); + items.AddRange(itemGroup); + } + + if (Device.RuntimePlatform == Device.iOS) + { + // HACK: [PS-536] Fix to avoid blank list after back navigation on unlocking with previous page info + // because of update to XF v5.0.0.2401 + GroupedItems.Clear(); + } + GroupedItems.ReplaceRange(items); + } + else + { + // HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list + var first = true; + var items = new List(); + foreach (var itemGroup in groupedItems) + { + if (!first) + { + items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); + } + else + { + first = false; + } + items.AddRange(itemGroup); + } + + if (groupedItems.Any()) + { + if (Device.RuntimePlatform == Device.iOS) + { + // HACK: [PS-536] Fix to avoid blank list after back navigation on unlocking with previous page info + // because of update to XF v5.0.0.2401 + GroupedItems.Clear(); + } + GroupedItems.ReplaceRange(new List { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) }); + GroupedItems.AddRange(items); + } + else + { + GroupedItems.Clear(); + } + } } finally { @@ -295,6 +394,25 @@ namespace Bit.App.Pages SyncRefreshing = false; } + public async Task VaultFilterOptionsAsync() + { + var options = new List { AppResources.AllVaults, AppResources.MyVault }; + if (_organizations.Any()) + { + options.AddRange(_organizations.Select(o => o.Name)); + } + var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null, + options.ToArray()); + if (selection == AppResources.Cancel || + (_vaultFilterSelection == null && selection == AppResources.AllVaults) || + (_vaultFilterSelection != null && _vaultFilterSelection == selection)) + { + return; + } + VaultFilterDescription = selection; + await LoadAsync(); + } + public async Task SelectCipherAsync(CipherView cipher) { var page = new ViewPage(cipher.Id); @@ -321,25 +439,26 @@ namespace Bit.App.Pages default: break; } - var page = new GroupingsPage(false, type, null, null, title); + var page = new GroupingsPage(false, type, null, null, title, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectFolderAsync(FolderView folder) { - var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name); + var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectCollectionAsync(Core.Models.View.CollectionView collection) { - var page = new GroupingsPage(false, null, null, collection.Id, collection.Name); + var page = new GroupingsPage(false, null, null, collection.Id, collection.Name, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectTrashAsync() { - var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true); + var page = new GroupingsPage(false, null, null, null, AppResources.Trash, _vaultFilterSelection, null, + true); await Page.Navigation.PushAsync(page); } @@ -377,8 +496,8 @@ namespace Bit.App.Pages private async Task LoadDataAsync() { + var orgId = await FillAllCiphersAndGetOrgIdIfNeededAsync(); NoDataText = AppResources.NoItems; - _allCiphers = await _cipherService.GetAllDecryptedAsync(); HasCiphers = _allCiphers.Any(); FavoriteCiphers?.Clear(); NoFolderCiphers?.Clear(); @@ -392,12 +511,11 @@ namespace Bit.App.Pages if (MainPage) { - Folders = await _folderService.GetAllDecryptedAsync(); - NestedFolders = await _folderService.GetAllNestedAsync(); + await FillFoldersAndCollectionsAsync(orgId); + NestedFolders = await _folderService.GetAllNestedAsync(Folders); HasFolders = NestedFolders.Any(f => f.Node?.Id != null); - Collections = await _collectionService.GetAllDecryptedAsync(); - NestedCollections = await _collectionService.GetAllNestedAsync(Collections); - HasCollections = NestedCollections.Any(); + NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null; + HasCollections = NestedCollections?.Any() ?? false; } else { @@ -517,6 +635,63 @@ namespace Bit.App.Pages } } + private async Task FillAllCiphersAndGetOrgIdIfNeededAsync() + { + string orgId = null; + var decCiphers = await _cipherService.GetAllDecryptedAsync(); + if (IsVaultFilterMyVault) + { + _allCiphers = decCiphers.Where(c => c.OrganizationId == null).ToList(); + } + else if (IsVaultFilterOrgVault) + { + orgId = GetVaultFilterOrgId(); + _allCiphers = decCiphers.Where(c => c.OrganizationId == orgId).ToList(); + } + else + { + _allCiphers = decCiphers; + } + return orgId; + } + + private async Task FillFoldersAndCollectionsAsync(string orgId) + { + var decFolders = await _folderService.GetAllDecryptedAsync(); + var decCollections = await _collectionService.GetAllDecryptedAsync(); + if (IsVaultFilterMyVault) + { + Folders = BuildFolders(decFolders); + Collections = null; + } + else if (IsVaultFilterOrgVault && !string.IsNullOrWhiteSpace(orgId)) + { + Folders = BuildFolders(decFolders); + Collections = decCollections?.Where(c => c.OrganizationId == orgId).ToList(); + } + else + { + Folders = decFolders; + Collections = decCollections; + } + } + + private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault; + + private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults && + _vaultFilterSelection != AppResources.MyVault; + + private string GetVaultFilterOrgId() + { + return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id; + } + + private List BuildFolders(List decFolders) + { + var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList(); + return folders.Any() ? folders : null; + } + private async void CipherOptionsAsync(CipherView cipher) { if ((Page as BaseContentPage).DoOnce()) diff --git a/src/App/Pages/Vault/GroupingsPage/IGroupingsPageListItem.cs b/src/App/Pages/Vault/GroupingsPage/IGroupingsPageListItem.cs new file mode 100644 index 000000000..56cb68d87 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/IGroupingsPageListItem.cs @@ -0,0 +1,6 @@ +namespace Bit.App.Pages +{ + public interface IGroupingsPageListItem + { + } +} diff --git a/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs b/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs index ea444c3d6..27f271da3 100644 --- a/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs +++ b/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs @@ -23,7 +23,8 @@ namespace Bit.App.Pages protected override async void OnAppearing() { base.OnAppearing(); - await LoadOnAppearedAsync(_mainLayout, true, async () => { + await LoadOnAppearedAsync(_mainLayout, true, async () => + { await _vm.InitAsync(); }); } diff --git a/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs index 2dc086322..bc415da2a 100644 --- a/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs +++ b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs @@ -1,9 +1,9 @@ -using Bit.App.Resources; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System.Collections.Generic; -using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages @@ -41,8 +41,11 @@ namespace Bit.App.Pages { var cipher = await _cipherService.GetAsync(CipherId); var decCipher = await cipher.DecryptAsync(); - History.ResetWithRange(decCipher.PasswordHistory ?? new List()); - ShowNoData = History.Count == 0; + Device.BeginInvokeOnMainThread(() => + { + History.ResetWithRange(decCipher.PasswordHistory ?? new List()); + ShowNoData = History.Count == 0; + }); } private async void CopyAsync(PasswordHistoryView ph) diff --git a/src/App/Pages/Vault/ScanPage.xaml.cs b/src/App/Pages/Vault/ScanPage.xaml.cs index 30d6fb09f..cf7e7a68e 100644 --- a/src/App/Pages/Vault/ScanPage.xaml.cs +++ b/src/App/Pages/Vault/ScanPage.xaml.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.AppCenter.Crashes; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages @@ -14,6 +15,8 @@ namespace Bit.App.Pages private CancellationTokenSource _autofocusCts; private Task _continuousAutofocusTask; + private readonly LazyResolve _logger = new LazyResolve("logger"); + public ScanPage(Action callback) { _callback = callback; @@ -61,9 +64,7 @@ namespace Bit.App.Pages catch (TaskCanceledException) { } catch (Exception ex) { -#if !FDROID - Crashes.TrackError(ex); -#endif + _logger.Value.Exception(ex); } }, autofocusCts.Token); } diff --git a/src/App/Pages/Vault/SharePageViewModel.cs b/src/App/Pages/Vault/SharePageViewModel.cs index 1f37f088a..c7e3cfbb1 100644 --- a/src/App/Pages/Vault/SharePageViewModel.cs +++ b/src/App/Pages/Vault/SharePageViewModel.cs @@ -1,13 +1,13 @@ -using Bit.App.Abstractions; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Bit.App.Pages { @@ -16,7 +16,7 @@ namespace Bit.App.Pages private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly ICollectionService _collectionService; - private readonly IUserService _userService; + private readonly IOrganizationService _organizationService; private readonly IPlatformUtilsService _platformUtilsService; private CipherView _cipher; private int _organizationSelectedIndex; @@ -28,7 +28,7 @@ namespace Bit.App.Pages { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); - _userService = ServiceContainer.Resolve("userService"); + _organizationService = ServiceContainer.Resolve("organizationService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _collectionService = ServiceContainer.Resolve("collectionService"); Collections = new ExtendedObservableCollection(); @@ -67,7 +67,7 @@ namespace Bit.App.Pages var allCollections = await _collectionService.GetAllDecryptedAsync(); _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); - var orgs = await _userService.GetAllOrganizationAsync(); + var orgs = await _organizationService.GetAllAsync(); OrganizationOptions = orgs.OrderBy(o => o.Name) .Where(o => o.Enabled && o.Status == OrganizationUserStatusType.Confirmed) .Select(o => new KeyValuePair(o.Name, o.Id)).ToList(); @@ -110,7 +110,7 @@ namespace Bit.App.Pages await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds); await _deviceActionService.HideLoadingAsync(); var movedItemToOrgText = string.Format(AppResources.MovedItemToOrg, cipherView.Name, - (await _userService.GetOrganizationAsync(OrganizationId)).Name); + (await _organizationService.GetAsync(OrganizationId)).Name); _platformUtilsService.ShowToast("success", null, movedItemToOrgText); await Page.Navigation.PopModalAsync(); return true; diff --git a/src/App/Pages/Vault/ViewPage.xaml b/src/App/Pages/Vault/ViewPage.xaml index 79d36d551..d581cd2f8 100644 --- a/src/App/Pages/Vault/ViewPage.xaml +++ b/src/App/Pages/Vault/ViewPage.xaml @@ -145,6 +145,7 @@ Grid.RowSpan="2" AutomationProperties.IsInAccessibleTree="True" AutomationProperties.Name="{u:I18n ToggleVisibility}" + AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" IsVisible="{Binding Cipher.ViewPassword}" /> {AppResources.Attachments}; + var options = new List { AppResources.Attachments }; if (_vm.Cipher.OrganizationId == null) { options.Add(AppResources.Clone); diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 3caadcf39..ffced6d18 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -5,16 +5,12 @@ using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; +using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.Core; using Xamarin.Forms; namespace Bit.App.Pages @@ -23,7 +19,7 @@ namespace Bit.App.Pages { private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ITotpService _totpService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuditService _auditService; @@ -53,7 +49,7 @@ namespace Bit.App.Pages { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _totpService = ServiceContainer.Resolve("totpService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _auditService = ServiceContainer.Resolve("auditService"); @@ -124,7 +120,8 @@ namespace Bit.App.Pages set => SetProperty(ref _showPassword, value, additionalPropertyNames: new string[] { - nameof(ShowPasswordIcon) + nameof(ShowPasswordIcon), + nameof(PasswordVisibilityAccessibilityText) }); } public bool ShowCardNumber @@ -217,6 +214,7 @@ namespace Bit.App.Pages public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; + public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string TotpCodeFormatted { get => _totpCodeFormatted; @@ -237,7 +235,7 @@ namespace Bit.App.Pages set { SetProperty(ref _totpLow, value); - Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"]; + Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"]; } } public bool IsDeleted => Cipher.IsDeleted; @@ -253,7 +251,7 @@ namespace Bit.App.Pages return false; } Cipher = await cipher.DecryptAsync(); - CanAccessPremium = await _userService.CanAccessPremiumAsync(); + CanAccessPremium = await _stateService.CanAccessPremiumAsync(); Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList(); if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) && @@ -289,7 +287,7 @@ namespace Bit.App.Pages public async void TogglePassword() { - if (! await PromptPasswordAsync()) + if (!await PromptPasswordAsync()) { return; } @@ -617,7 +615,7 @@ namespace Bit.App.Pages _attachmentData = null; _attachmentFilename = null; } - + private async void CopyAsync(string id, string text = null) { if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync()) @@ -758,12 +756,12 @@ namespace Bit.App.Pages { if (IsBooleanType) { - return _field.Value == "true" ? "" : ""; + return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare; } else if (IsLinkedType) { var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault()); - return " " + _i18nService.T(i18nKey); + return BitwardenIcons.Link + _i18nService.T(i18nKey); } else { @@ -772,6 +770,8 @@ namespace Bit.App.Pages } } + public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value); + public Command ToggleHiddenValueCommand { get; set; } public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index bde127d61..a8a6d45fa 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -311,6 +311,30 @@ namespace Bit.App.Resources { } } + public static string RemoveAccount { + get { + return ResourceManager.GetString("RemoveAccount", resourceCulture); + } + } + + public static string RemoveAccountConfirmation { + get { + return ResourceManager.GetString("RemoveAccountConfirmation", resourceCulture); + } + } + + public static string AccountAlreadyAdded { + get { + return ResourceManager.GetString("AccountAlreadyAdded", resourceCulture); + } + } + + public static string SwitchToAlreadyAddedAccountConfirmation { + get { + return ResourceManager.GetString("SwitchToAlreadyAddedAccountConfirmation", resourceCulture); + } + } + public static string MasterPassword { get { return ResourceManager.GetString("MasterPassword", resourceCulture); @@ -2369,15 +2393,21 @@ namespace Bit.App.Resources { } } - public static string SearchFolder { + public static string SearchFileSends { get { - return ResourceManager.GetString("SearchFolder", resourceCulture); + return ResourceManager.GetString("SearchFileSends", resourceCulture); } } - public static string SearchType { + public static string SearchTextSends { get { - return ResourceManager.GetString("SearchType", resourceCulture); + return ResourceManager.GetString("SearchTextSends", resourceCulture); + } + } + + public static string SearchGroup { + get { + return ResourceManager.GetString("SearchGroup", resourceCulture); } } @@ -3275,6 +3305,12 @@ namespace Bit.App.Resources { } } + public static string Text { + get { + return ResourceManager.GetString("Text", resourceCulture); + } + } + public static string TypeText { get { return ResourceManager.GetString("TypeText", resourceCulture); @@ -3305,6 +3341,30 @@ namespace Bit.App.Resources { } } + public static string FileTypeIsSelected { + get { + return ResourceManager.GetString("FileTypeIsSelected", resourceCulture); + } + } + + public static string FileTypeIsNotSelected { + get { + return ResourceManager.GetString("FileTypeIsNotSelected", resourceCulture); + } + } + + public static string TextTypeIsSelected { + get { + return ResourceManager.GetString("TextTypeIsSelected", resourceCulture); + } + } + + public static string TextTypeIsNotSelected { + get { + return ResourceManager.GetString("TextTypeIsNotSelected", resourceCulture); + } + } + public static string DeletionDate { get { return ResourceManager.GetString("DeletionDate", resourceCulture); @@ -3725,6 +3785,54 @@ namespace Bit.App.Resources { } } + public static string AddAccount { + get { + return ResourceManager.GetString("AddAccount", resourceCulture); + } + } + + public static string AccountUnlocked { + get { + return ResourceManager.GetString("AccountUnlocked", resourceCulture); + } + } + + public static string AccountLocked { + get { + return ResourceManager.GetString("AccountLocked", resourceCulture); + } + } + + public static string AccountLoggedOut { + get { + return ResourceManager.GetString("AccountLoggedOut", resourceCulture); + } + } + + public static string AccountSwitchedAutomatically { + get { + return ResourceManager.GetString("AccountSwitchedAutomatically", resourceCulture); + } + } + + public static string AccountLockedSuccessfully { + get { + return ResourceManager.GetString("AccountLockedSuccessfully", resourceCulture); + } + } + + public static string AccountLoggedOutSuccessfully { + get { + return ResourceManager.GetString("AccountLoggedOutSuccessfully", resourceCulture); + } + } + + public static string AccountRemovedSuccessfully { + get { + return ResourceManager.GetString("AccountRemovedSuccessfully", resourceCulture); + } + } + public static string DeleteAccount { get { return ResourceManager.GetString("DeleteAccount", resourceCulture); @@ -3820,5 +3928,101 @@ namespace Bit.App.Resources { return ResourceManager.GetString("EnterTheVerificationCodeThatWasSentToYourEmail", resourceCulture); } } + + public static string ReportCrashLogs { + get { + return ResourceManager.GetString("ReportCrashLogs", resourceCulture); + } + } + + public static string ReportCrashLogsDescription { + get { + return ResourceManager.GetString("ReportCrashLogsDescription", resourceCulture); + } + } + + public static string OptionsExpanded { + get { + return ResourceManager.GetString("OptionsExpanded", resourceCulture); + } + } + + public static string OptionsCollapsed { + get { + return ResourceManager.GetString("OptionsCollapsed", resourceCulture); + } + } + + public static string UppercaseAtoZ { + get { + return ResourceManager.GetString("UppercaseAtoZ", resourceCulture); + } + } + + public static string LowercaseAtoZ { + get { + return ResourceManager.GetString("LowercaseAtoZ", resourceCulture); + } + } + + public static string NumbersZeroToNine { + get { + return ResourceManager.GetString("NumbersZeroToNine", resourceCulture); + } + } + + public static string SpecialCharacters { + get { + return ResourceManager.GetString("SpecialCharacters", resourceCulture); + } + } + + public static string TapToGoBack { + get { + return ResourceManager.GetString("TapToGoBack", resourceCulture); + } + } + + public static string PasswordIsVisibleTapToHide { + get { + return ResourceManager.GetString("PasswordIsVisibleTapToHide", resourceCulture); + } + } + + public static string PasswordIsNotVisibleTapToShow { + get { + return ResourceManager.GetString("PasswordIsNotVisibleTapToShow", resourceCulture); + } + } + + public static string FilterByVault { + get { + return ResourceManager.GetString("FilterByVault", resourceCulture); + } + } + + public static string AllVaults { + get { + return ResourceManager.GetString("AllVaults", resourceCulture); + } + } + + public static string Vaults { + get { + return ResourceManager.GetString("Vaults", resourceCulture); + } + } + + public static string VaultFilterDescription { + get { + return ResourceManager.GetString("VaultFilterDescription", resourceCulture); + } + } + + public static string All { + get { + return ResourceManager.GetString("All", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.af.resx b/src/App/Resources/AppResources.af.resx index 9068877b6..9d0d8633d 100644 --- a/src/App/Resources/AppResources.af.resx +++ b/src/App/Resources/AppResources.af.resx @@ -275,6 +275,18 @@ Is u seker u wil uitteken? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Hoofwagwoord Label for a master password. @@ -2092,6 +2104,30 @@ Een of meer organisasiebeleide verhoed u om u persoonlike kluis uit te stuur. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Skrap rekening diff --git a/src/App/Resources/AppResources.az.resx b/src/App/Resources/AppResources.az.resx index 1d73a97e1..1f48ad319 100644 --- a/src/App/Resources/AppResources.az.resx +++ b/src/App/Resources/AppResources.az.resx @@ -275,6 +275,18 @@ Çıxış etmək istədiyinizə əminsiniz? + + Hesabı sil + + + Bu istifadəçini silmək istədiyinizə əminsiniz? + + + Hesab artıq əlavə edildi + + + Buna indi keçmək istəyirsiniz? + Ana parol Label for a master password. @@ -2092,6 +2104,30 @@ Bir və ya daha çox təşkilat siyasəti, fərdi anbarınızı ixrac etməyinizin qarşısını alır. + + Hesab əlavə et + + + Kilidi açıldı + + + Kilidli + + + Çıxış edildi + + + Növbəti mövcud hesaba keçildi + + + Hesab kilidlidir + + + Hesabdan uğurla çıxış edildi + + + Hesab uğurla silindi + Hesabı sil diff --git a/src/App/Resources/AppResources.be.resx b/src/App/Resources/AppResources.be.resx index 05afd0783..8883b51c3 100644 --- a/src/App/Resources/AppResources.be.resx +++ b/src/App/Resources/AppResources.be.resx @@ -275,6 +275,18 @@ Вы ўпэўнены, што хочаце выйсці? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Асноўны пароль Label for a master password. @@ -2092,6 +2104,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.bg.resx b/src/App/Resources/AppResources.bg.resx index 1eecdafa7..3bacc6460 100644 --- a/src/App/Resources/AppResources.bg.resx +++ b/src/App/Resources/AppResources.bg.resx @@ -275,6 +275,18 @@ Сигурни ли сте, че искате да се отпишете? + + Премахване на регистрацията + + + Наистина ли искате да премахнете тази регистрация? + + + Регистрацията вече е добавена + + + Искате ли да превключите към нея сега? + Главна парола Label for a master password. @@ -2093,6 +2105,30 @@ Една или повече от настройките на организацията Ви не позволяват да изнасяте личния си трезор. + + Добавяне на регистрация + + + Отключено + + + Заключено + + + Отписано + + + Превключено към следващата налична регистрация + + + Регистрацията е заключена + + + Отписването от регистрацията беше успешно + + + Регистрацията беше премахната успешно + Изтриване на регистрацията diff --git a/src/App/Resources/AppResources.bn.resx b/src/App/Resources/AppResources.bn.resx index 7d4094c9c..15255c198 100644 --- a/src/App/Resources/AppResources.bn.resx +++ b/src/App/Resources/AppResources.bn.resx @@ -275,6 +275,18 @@ আপনি লগ আউট করতে চান? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + প্রধান পাসওয়ার্ড Label for a master password. @@ -1150,10 +1162,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.bs.resx b/src/App/Resources/AppResources.bs.resx index a5ee120a5..3a11f45f2 100644 --- a/src/App/Resources/AppResources.bs.resx +++ b/src/App/Resources/AppResources.bs.resx @@ -164,7 +164,7 @@ Title for page that we use to give credit to resources that we use. - Obriši + Izbriši Delete an entity (verb). @@ -186,37 +186,37 @@ Short label for an email address. - Adresa e-pošte + E-Mail adresa Full label for a email address. - Pošaljite nam e-poštu + Pošaljite nam E-Mail - Pošaljite nam e-poštu direktno da biste dobili pomoć ili ostavili povratne informacije. + Pošaljite nam E-Mail direktno da biste dobili pomoć ili ostavili povratne informacije. Unesite Vaš PIN kod. - Omiljene + Omiljene stavke Title for your favorite items in the vault. - Podnesite izveštaj o grešci + Podnesite izvještaj o greški - Otvorite izdanje u našem GitHub spremištu. + Prijavi problem u našem GitHub repozitoriju. - Verifikujte pomoću otiska prsta. + Upotrijebite otisak prsta da biste nastavili. Folder Label for a folder. - Kreirana je novi folder. + Kreiran je novi Folder. Folder je izbrisan. @@ -226,20 +226,20 @@ Items that have no folder specified go in this special "catch-all" folder. - Fascikle + Folderi Folder ažuriran. - Idite na vebsajt + Posjetite web stranicu The button text that allows user to launch the website to their web browser. Pomoć i povratne informacije - Sakrijte + Sakrij Hide a secret value that is currently shown (password). @@ -247,7 +247,7 @@ Description message for the alert when internet connection is required to continue. - Internet veza obavezna + Neophodna je internet veza Title for the alert when internet connection is required to continue. @@ -257,7 +257,7 @@ Nevažeći PIN. Pokušajte ponovo. - Pokrenite + Otvori web stranicu The button text that allows user to launch the website to their web browser. @@ -269,12 +269,24 @@ Title for login page. (noun) - Odjavite se + Odjava The log out button text (verb). Da li ste sigurni da želite da se odjavite? + + Obriši račun + + + Da li ste sigurni da želite ukloniti ovaj račun? + + + Račun je već dodan + + + Da li želite da se prebacite na njega sada? + Glavna lozinka Label for a master password. @@ -701,7 +713,7 @@ Otključajte pomoću PIN koda - Validacija + Potvrđivanje Message shown when interacting with the server @@ -1012,10 +1024,10 @@ April - Avgust + August - Brend + Vrsta kartice Ime vlasnika kartice @@ -1039,7 +1051,7 @@ Mjesec roka upotrebe - Godina roka upotrebe + Godina isteka Februar @@ -1099,13 +1111,13 @@ Septembar - JMBG + Broj socijalnog osiguranja / JMBG Država / Pokrajina - Titula + Naslov Poštanski broj @@ -1187,7 +1199,7 @@ Skriven - Povezano + Povezano sa Tekst @@ -2092,6 +2104,30 @@ Jedno ili više pravila organizacija onemogućuje izvoz osobnog trezora. + + Dodaj račun + + + Otključano + + + Zaključano + + + Odjavljeno + + + Prebačeni ste na sljedeći dostupan račun + + + Račun zaključan + + + Uspješno odjavljeni sa računa + + + Račun je uspješno uklonjen + Obriši račun diff --git a/src/App/Resources/AppResources.ca.resx b/src/App/Resources/AppResources.ca.resx index eb214940d..50b44fb16 100644 --- a/src/App/Resources/AppResources.ca.resx +++ b/src/App/Resources/AppResources.ca.resx @@ -275,6 +275,18 @@ Segur que voleu tancar la sessió? + + Suprimeix el compte + + + Voleu suprimir el compte? + + + El compte ja s'ha afegit + + + T'agradaria canviar-lo ara? + Contrasenya mestra Label for a master password. @@ -2092,6 +2104,30 @@ Una o més polítiques d'organització us impedeixen exportar la vostra caixa forta. + + Afig compte + + + Desbloquejat + + + Bloquejat + + + Sessió tancada + + + S'ha canviat al següent compte disponible + + + Compte bloquejat + + + El compte s'ha tancat correctament + + + El compte s'ha suprimit correctament + Suprimeix el compte diff --git a/src/App/Resources/AppResources.cs.resx b/src/App/Resources/AppResources.cs.resx index a216c4bf9..5cc2b11d3 100644 --- a/src/App/Resources/AppResources.cs.resx +++ b/src/App/Resources/AppResources.cs.resx @@ -275,6 +275,18 @@ Opravdu se chcete odhlásit? + + Odebrat účet + + + Opravdu si přejete tento účet odebrat? + + + Účet byl již přidán + + + Would you like to switch to it now? + Hlavní heslo Label for a master password. @@ -2092,6 +2104,30 @@ Jedna nebo více zásad organizace vám brání v exportu vašeho osobního trezoru. + + Přidat účet + + + Odemčen + + + Uzamčen + + + Odhlášen + + + Přepnuto na další dostupný účet + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Odstranit účet diff --git a/src/App/Resources/AppResources.da.resx b/src/App/Resources/AppResources.da.resx index 6af06fb14..62fbe7124 100644 --- a/src/App/Resources/AppResources.da.resx +++ b/src/App/Resources/AppResources.da.resx @@ -275,6 +275,18 @@ Er du sikker på, at du vil logge ud? + + Fjern konto + + + Er du sikker på, at du vil fjerne denne konto? + + + Konto allerede tilføjet + + + Vil du skifte til den nu? + Hovedadgangskode Label for a master password. @@ -1150,10 +1162,10 @@ Autoudfyld hjælpefunktion - Bitwarden autoudfyld tjenesten bruger Androids AutoFyld framework til at hjælpe med at udfylde logins, kreditkort og identitetsoplysninger i andre apps på din enhed. + Bitwarden autoudfyld tjenesten bruger Androids AutoFyld framework til at hjælpe med at udfylde loginoplysninger i andre apps på din enhed. - Brug Bitwarden autoudfyld tjenesten til at udfylde logins, kreditkort og identitetsoplysninger i andre apps. + Brug Bitwarden autoudfyld tjenesten til at udfylde loginoplysninger i andre apps. Åbn autoudfyld indstillinger @@ -2092,6 +2104,30 @@ En eller flere organisationspolitikker forhindrer eksport af din personlige boks. + + Tilføj konto + + + Låst op + + + Låst + + + Logget ud + + + Skiftede til næste tilgængelige konto + + + Konto låst + + + Konto logget ud + + + Konto fjernet + Slet konto diff --git a/src/App/Resources/AppResources.de.resx b/src/App/Resources/AppResources.de.resx index 0a46eff61..1544aa920 100644 --- a/src/App/Resources/AppResources.de.resx +++ b/src/App/Resources/AppResources.de.resx @@ -275,6 +275,18 @@ Bist du sicher, dass du dich abmelden möchtest? + + Konto entfernen + + + Möchten Sie das Konto wirklich entfernen? + + + Konto bereits hinzugefügt + + + Möchten Sie jetzt darauf umschalten? + Masterpasswort Label for a master password. @@ -1287,7 +1299,7 @@ 1. Gehe in die iOS Einstellungen - 2. Drücke "Passwörter & Accounts" + 2. Tippe auf "Passwörter" 3. Tippe auf "Automatisch ausfüllen" @@ -2092,6 +2104,30 @@ Eine oder mehrere Unternehmensrichtlinien verhindern es, dass du deinen persönlichen Tresor exportieren kannst. + + Konto hinzufügen + + + Entsperrt + + + Gesperrt + + + Ausgeloggt + + + Zum nächsten verfügbaren Konto gewechselt + + + Konto gesperrt + + + Konto erfolgreich abgemeldet + + + Konto erfolgreich gelöscht + Konto löschen diff --git a/src/App/Resources/AppResources.el.resx b/src/App/Resources/AppResources.el.resx index 2000211d6..554bcb0a6 100644 --- a/src/App/Resources/AppResources.el.resx +++ b/src/App/Resources/AppResources.el.resx @@ -276,6 +276,18 @@ Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε; + + Αφαίρεση λογαριασμού + + + Είστε βέβαιοι ότι θέλετε να καταργήσετε αυτόν τον λογαριασμό? + + + Ο Λογαριασμός Προστέθηκε Ήδη + + + Θα θέλατε να το αλλάξετε τώρα? + Κύριος Κωδικός Label for a master password. @@ -2093,6 +2105,30 @@ Μία ή περισσότερες οργανωτικές πολιτικές αποτρέπουν την εξαγωγή του προσωπικού vault. + + Προσθήκη Λογαριασμού + + + Ξεκλειδώθηκε + + + Κλειδωμένο + + + Αποσυνδεθήκατε + + + Μετάβαση στον επόμενο διαθέσιμο λογαριασμό + + + Κλειδωμένος Λογαριασμός + + + Ο λογαριασμός αποσυνδέθηκε επιτυχώς + + + Ο λογαριασμός αφαιρέθηκε επιτυχώς + Διαγραφή Λογαριασμού diff --git a/src/App/Resources/AppResources.en-IN.resx b/src/App/Resources/AppResources.en-IN.resx index 102239845..d388d7f4a 100644 --- a/src/App/Resources/AppResources.en-IN.resx +++ b/src/App/Resources/AppResources.en-IN.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master password Label for a master password. @@ -2105,6 +2117,30 @@ One or more organisation policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.es.resx b/src/App/Resources/AppResources.es.resx index d33e75830..3bd88ff26 100644 --- a/src/App/Resources/AppResources.es.resx +++ b/src/App/Resources/AppResources.es.resx @@ -275,6 +275,18 @@ ¿Estás seguro de querer cerrar sesión? + + Eliminar cuenta + + + ¿Está seguro que desea eliminar esta cuenta? + + + Cuenta ya añadida + + + ¿Quieres cambiarlo ahora? + Contraseña maestra Label for a master password. @@ -1153,7 +1165,7 @@ El servicio de autorellenado de Bitwarden utiliza el marco de autocompletado de Android para ayudarte a rellenar entradas, tarjetas de crédito e identidades en otras aplicaciones de tu dispositivo. - Utiliza el servicio de accesibilidad de Bitwarden para autorellenar entradas. + Utiliza el servicio de accesibilidad de Bitwarden para autorrellenar entradas. Abrir opciones de autorellenado @@ -2051,7 +2063,7 @@ Remueve Contraseña Maestra - {0} is using SSO with customer-managed encryption. Continuing will remove your Master Password from your account and require SSO to login. + {0} está usando SSO con el cifrado administrado por el cliente. Continuar eliminará su contraseña maestra de su cuenta y requerirá SSO para iniciar sesión. Si no desea eliminar su contraseña maestra, puede abandonar esta organización. @@ -2092,11 +2104,35 @@ Una o más políticas de organización impiden que usted exporte su caja fuerte personal. + + Añadir cuenta + + + Desbloqueado + + + Bloqueado + + + Desconectado + + + Cambiado a la siguiente cuenta disponible + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + - Delete Account + Eliminar cuenta - Deleting your account is permanent + Eliminar tu cuenta es permanente Tu cuenta y todos los datos asociados serán borrados e irrecuperables. ¿Estás seguro de que quieres continuar? @@ -2111,7 +2147,7 @@ Código de verificación no válido. - Request one-time password + Solicitar contraseña de una sola vez Enviar código @@ -2120,24 +2156,24 @@ Enviando - Copy Send link on save + Copiar enlace Enviar al guardar - Sending code + Enviando código... - Verifying + Verificando... - Resend Code + Reenviar código - A verification code was sent to your email + Se ha enviado un código de verificación a tu correo electrónico - An error occurred while sending a verification code to your email. Please try again + Se ha producido un error al enviar un código de verificación a tu correo electrónico. Por favor, inténtalo de nuevo - Enter the verification code that was sent to your email + Introduce el código de verificación enviado a tu correo electrónico diff --git a/src/App/Resources/AppResources.et.resx b/src/App/Resources/AppResources.et.resx index b0aedbeda..3d47f34d3 100644 --- a/src/App/Resources/AppResources.et.resx +++ b/src/App/Resources/AppResources.et.resx @@ -275,6 +275,18 @@ Oled kindel, et soovid välja logida? + + Eemalda konto + + + Oled kindel, et soovid selle konto eemaldada? + + + Konto on juba lisatud + + + Kas soovid kohe vahetada? + Ülemparool Label for a master password. @@ -827,7 +839,7 @@ Kaheastmelise sisselogimise valikud - Kasuta teist kaheastmelist sisselogimise meetodit + Kasuta teist meetodit Kinnitus e-kirja saatmine ebaõnnestus. Proovi uuesti. @@ -2092,6 +2104,30 @@ Üks või enam organisatsiooni poliitikat ei võimalda sul oma personaalset hoidlat eksportida. + + Lisa konto + + + Lukustamata + + + Lukustatud + + + Välja logitud + + + Vahetati järgmise saadaoleva konto peale + + + Kasutajakonto on lukus + + + Konto on edukalt välja logitud + + + Konto on edukalt eemaldatud + Kustuta konto diff --git a/src/App/Resources/AppResources.fa.resx b/src/App/Resources/AppResources.fa.resx index ec81864ac..24713ec5d 100644 --- a/src/App/Resources/AppResources.fa.resx +++ b/src/App/Resources/AppResources.fa.resx @@ -275,6 +275,18 @@ آیا مطمئنید که می‌خواهید خارج شوید؟ + + حذف حساب + + + آیا از حذف این حساب اطمینان دارید؟ + + + حساب قبلا اضافه شده + + + آیا می خواهید همین الان تعویض کنید؟ + کلمه عبور اصلی Label for a master password. @@ -2093,6 +2105,30 @@ یک یا چند خط مشی سازمان از صادرات گاوصندوق شخصی شما جلوگیری می کند. + + افزودن حساب کاربری + + + باز شده + + + قفل شده + + + خارج شده + + + به حساب بعدی موجود تغییر کرد + + + حساب قفل شده است + + + حساب کاربری با موفقیت خارج شد + + + حساب کاربری با موفقیت حذف شد + حذف حساب diff --git a/src/App/Resources/AppResources.fi.resx b/src/App/Resources/AppResources.fi.resx index 59ac5b53e..c69d72d54 100644 --- a/src/App/Resources/AppResources.fi.resx +++ b/src/App/Resources/AppResources.fi.resx @@ -275,6 +275,18 @@ Haluatko varmasti kirjautua ulos? + + Poista tili + + + Haluatko varmasti poistaa tämän tilin? + + + Tili on jo lisätty + + + Haluatko vaihtaa siihen nyt? + Pääsalasana Label for a master password. @@ -1150,10 +1162,10 @@ Automaattisen täytön esteettömyyspalvelu - Bitwardenin automaattisen täytön palvelu käyttää Android Autofill Framework -rajapintaa kirjautumistietojen, korttien ja henkilöllisyyksien täyttöön laitteesi sovelluksissa. + Bitwardenin automaattisen täytön palvelu käyttää Android Autofill Framework -rajapintaa kirjautumistietojen täyttöön laitteen sovelluksissa. - Käytä Bitwardenin esteettömyyspalvelua kirjautumistietojen, korttien ja henkilöllisyyksien automaattiseen täyttöön sovelluksissa. + Käytä Bitwardenin automaattisen täytön palvelua kirjautumistietojen täyttöön sovelluksissa. Avaa "Automaattinen täyttö -palvelu" -asetukset @@ -1287,7 +1299,7 @@ 1. Siirry iOS:n "Asetukset" -sovellukseen - 2. Napauta "Salasanat ja tilit" + 2. Napauta "Salasanat" 3. Napauta "Täytä salasanat automaattisesti" @@ -1526,7 +1538,7 @@ Oletus (järjestelmä) - Kopioi muistiinpanot + Kopioi merkinnät Poistu @@ -1557,7 +1569,7 @@ Kun sovellus käynnistetään uudelleen - Automaattinen täyttö tekee Bitwarden-holvisi käytöstä sivustoilla ja muissa sovelluksissa helppoa. Näyttää siltä, ettei automaattista täyttöä ole otettu käyttöön. Voit tehdä sen "Asetukset" -näytöstä. + Automaattinen täyttö tekee Bitwarden-holvisi käytöstä sivustoilla ja muissa sovelluksissa helppoa. Näyttää siltä, ettei automaattista täyttöä ole otettu käyttöön. Voit tehdä sen "Asetukset" -ruudusta. Teema vaihtuu kun sovellus käynnistetään uudelleen. @@ -1919,11 +1931,11 @@ Salasana on poistettu. - Yksityiset muistiinpanot tästä Sendistä. + Yksityisiä merkintöjä tästä Sendistä. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Poista Send käytöstä, jottei kukaan voi avata sitä. + Poista Send käytöstä, jottei kukaan voi avata sitä 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1991,7 +2003,7 @@ Mukautettu - Jaa Send tallennettaessa. + Jaa Send tallennettaessa 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2003,7 +2015,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Piilota sähköpostiosoitteeni vastaanottajilta. + Piilota sähköpostiosoitteeni vastaanottajilta Yksi tai useampi organisaatiokäytäntö vaikuttaa Send-asetuksiisi. @@ -2092,6 +2104,30 @@ Yksi tai useampi organisaation käytäntö estää henkilökohtaisen holvisi viennin. + + Lisää tili + + + Avattu + + + Lukittu + + + Kirjauduttu ulos + + + Vaihdettu seuraavaan käytettävissä olevaan tiliin + + + Tili lukittu + + + Tilin uloskirjaus onnistui + + + Tilin poisto onnistui + Poista tili diff --git a/src/App/Resources/AppResources.fil.resx b/src/App/Resources/AppResources.fil.resx index a7a073d97..15372b1ba 100644 --- a/src/App/Resources/AppResources.fil.resx +++ b/src/App/Resources/AppResources.fil.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -1150,10 +1162,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.fr.resx b/src/App/Resources/AppResources.fr.resx index bea91d20c..45ea31e66 100644 --- a/src/App/Resources/AppResources.fr.resx +++ b/src/App/Resources/AppResources.fr.resx @@ -275,6 +275,18 @@ Êtes-vous sûr de vouloir vous déconnecter ? + + Retirer le compte + + + Êtes-vous sûr de vouloir retirer ce compte ? + + + Le compte a déjà été ajouté + + + Souhaitez-vous y passer maintenant ? + Mot de passe maître Label for a master password. @@ -1150,10 +1162,10 @@ Service d'accessibilité pour le remplissage automatique - Le service de remplissage automatique de Bitwarden utilise l'outil de remplissage automatique d'Android pour aider à saisir les identifiants, les cartes de crédit et les informations d'identité dans d'autres applications sur votre appareil. + Le service de saisie automatique de Bitwarden utilise l'outil de saisie automatique d'Android pour aider à saisir les identifiants, les cartes de crédit et les informations d'identité dans d'autres applis sur votre appareil. - Utilisez le service d'accessibilité de Bitwarden pour le remplissage automatique de vos identifiants. + Utilisez le service de remplissage automatique de Bitwarden pour remplir les informations de connexion dans d'autres applications. Ouvrir les paramètres de remplissage automatique @@ -1287,7 +1299,7 @@ 1. Allez dans l'application "Réglages" d'iOS - 2. Appuyez sur "Mots de passe et comptes" + 2. Appuyez sur "Mots de passes et comptes" 3. Appuyez sur "Préremplir mots de passe" @@ -1923,7 +1935,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Désactiver ce Send pour que personne ne puisse y accéder. + Désactiver cet envoi pour que personne ne puisse y accéder. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2092,6 +2104,30 @@ Une ou plusieurs politiques d'organisation vous empêchent d'exporter votre coffre personnel. + + Ajouter un compte + + + Déverrouillé + + + Verrouillé + + + Déconnecté + + + Passage au prochain compte disponible + + + Compte verrouillé + + + Compte déconnecté avec succès + + + Compte supprimé avec succès + Supprimer le compte diff --git a/src/App/Resources/AppResources.he.resx b/src/App/Resources/AppResources.he.resx index 7d6e9d217..fa6ee3939 100644 --- a/src/App/Resources/AppResources.he.resx +++ b/src/App/Resources/AppResources.he.resx @@ -275,6 +275,18 @@ האם אתה בטוח שברצונך להתנתק? + + הסר/י חשבון + + + האם את/ה בטוח/ה שברצונך להסיר חשבון זה? + + + החשבון כבר נוסף + + + האם תרצה לעבור אליו עכשיו? + סיסמה ראשית Label for a master password. @@ -1061,7 +1073,7 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". שם משפחה - Full Name + שם מלא מספר רשיון @@ -1188,7 +1200,7 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". מוסתר - Linked + מקושר טקסט @@ -1460,7 +1472,7 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". בטל נעילה - Unlock Vault + פתח נעילת כספת 30 דקות @@ -1479,7 +1491,7 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". הכספת שלך נעולה. הזן את קוד הPIN שלך כדי להמשיך. - Your vault is locked. Verify your identity to continue. + הכספת שלך נעולה. אמת את זהותך כדי להמשיך. כהה @@ -1622,13 +1634,13 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". הזן את הסיסמה הראשית שלך עבור יצוא המידע מהכספת. - Send a verification code to your email + שליחת קוד אימות לדוא״ל שלך - Code Sent! + קוד נשלח! - Confirm your identity to continue. + אשר את זהותך כדי להמשיך. הקובץ מכיל את פרטי הכספת שלך בפורמט לא מוצפן. מומלץ להעביר את הקובץ רק בדרכים מוצפנות, ומאוד לא מומלץ לשמור או לשלוח את הקובץ הזה בדרכים לא מוצפנות (כדוגמת סתם אימייל). מחק את הקובץ מיד לאחר שסיימת את השימוש בו. @@ -2054,19 +2066,19 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". כרגע לא ניתן לעדכן את הסיסמה - Remove Master Password + הסרת סיסמה ראשית - {0} is using SSO with customer-managed encryption. Continuing will remove your Master Password from your account and require SSO to login. + {0} משתמש/ת ב־SSO עם הצפנה בניהול לקוחות. המשך התהליך יסיר את סיסמת האב שלך מחשבונך וידרוש SSO כדי להתחבר. - If you do not want to remove your Master Password, you may leave this organization. + אם אינך רוצה להסיר את הסיסמה הראשית שלך, תוכל לעזוב את הארגון הזה. - Leave Organization + עזוב ארגון - Leave {0}? + לעזוב את {0}? אימות אינטרנט באמצעות FIDO2 @@ -2099,52 +2111,77 @@ Bitwarden בעזרת פתיחת חלון "הגדרות". מדיניות אחת או יותר של הארגון שלך מונעות ממך לייצא את הכספת האישית שלך. + + הוספת חשבון + + + פתוח + + + נעול + + + בוצעה יציאה + + + הוחלף לחשבון הזמין הבא + + + החשבון נעול + + + נותק בהצלחה מהחשבון + + + חשבון נמחק בהצלחה + - Delete Account + מחק חשבון - Deleting your account is permanent + מחיקת חשבונך היא לצמיתות - Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue? + החשבון שלך וכל הנתונים הקשורים יימחקו ולא ניתן יהיה לשחזרם. האם את/ה בטוח/ה שאת/ה רוצה להמשיך? + - Deleting your account + מוחק את החשבון שלך - Your account has been permanently deleted + חשבונך נמחק לצמיתות - Invalid Verification Code. + קוד אימות שגוי. - Request one-time password + בקשת סיסמה חד־פעמית - Send Code + שליחת קוד - Sending + בשליחה - Copy Send link on save + העתק קישור שליחה בעת שמירה - Sending code + שליחת קוד - Verifying + אימות - Resend Code + שליחת קוד בשנית - A verification code was sent to your email + קוד אימות נשלח לדוא"ל שלך - An error occurred while sending a verification code to your email. Please try again + אירעה שגיאה בעת שליחת קוד אימות לדוא"ל שלך. בבקשה נסה שוב - Enter the verification code that was sent to your email + הזן את קוד האימות שנשלח לדוא"ל שלך diff --git a/src/App/Resources/AppResources.hi.resx b/src/App/Resources/AppResources.hi.resx index 4eb028bc1..7f9c358e5 100644 --- a/src/App/Resources/AppResources.hi.resx +++ b/src/App/Resources/AppResources.hi.resx @@ -275,6 +275,18 @@ क्या आप वाकई लॉग आउट करना चाहते हैं? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + मास्टर / मुख्य पासवर्ड Label for a master password. @@ -1288,7 +1300,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2094,6 +2106,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.hr.resx b/src/App/Resources/AppResources.hr.resx index d9526b544..4086a21e4 100644 --- a/src/App/Resources/AppResources.hr.resx +++ b/src/App/Resources/AppResources.hr.resx @@ -275,6 +275,18 @@ Sigurno se želiš odjaviti? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Glavna lozinka Label for a master password. @@ -1153,7 +1165,7 @@ Usluga Bitwarden auto-ispune koristi Android Autofill Framework za pomoć pri ispunjavanju prijava, platnih kartica i identifikacijskih podataka u drugim aplikacijama na tvojem uređaju. - Koristi uslugu Bitwarden auto-ispune za ispunjavanje prijava, platnih kartica i identifikacijskih podataka u drugim aplikacijama. + Upotrijebite uslugu pristupačnosti usluge bitwarden da biste automatski ispunili prijave. Otvori postavke auto-ispune @@ -2092,6 +2104,30 @@ Jedno ili više pravila organizacija onemogućuje izvoz osobnog trezora. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Obriši račun diff --git a/src/App/Resources/AppResources.hu.resx b/src/App/Resources/AppResources.hu.resx index 418c7bc45..6179a83d3 100644 --- a/src/App/Resources/AppResources.hu.resx +++ b/src/App/Resources/AppResources.hu.resx @@ -275,6 +275,18 @@ Biztos, hogy ki szeretnél jelentkezni? + + Fiók eltávolítása + + + Biztosan törlésre kerüljön ez a fiók? + + + A fiók már hozzáadásra került. + + + Szeretnénk most átkapcsolni erre? + Mesterjelszó Label for a master password. @@ -1153,7 +1165,7 @@ A Bitwarden automatikus kitöltési szolgáltatása az Android automatikus kitöltő keretrendszerét használja a bejelentkezési adatok, hitelkártyaadatok és azonosítóadatok kitöltésére az eszközén telepített más alkalmazásokban. - A Bitwarden automatikus kitöltési szolgáltatásának használatával kitöltheti a bejelentkezési, hitelkártya- és azonosító adatokat más alkalmazásokban. + A Bitwarden automatikus kitöltési szolgáltatás használata a bejelentkezési-, hitelkártya- és azonosítóadatok kitöltéséhez más alkalmazásokban. Automatikus kitöltés beállításainak megnyitása @@ -2092,6 +2104,30 @@ Egy vagy több szervezeti házirend tiltja a személyes széf exportálását. + + Fiók hozzáadása + + + Feloldva + + + Lezárva + + + Megtörtént a kijelentkezés. + + + Megtörtént az átkapcsolás a következő elérhető fiókra. + + + A fiók lezárásra került. + + + A fiókból kijelentkezés sikeres volt. + + + A fiók eltávolítása sikeres volt. + Fiók törlése diff --git a/src/App/Resources/AppResources.id.resx b/src/App/Resources/AppResources.id.resx index 33360ee67..3db293049 100644 --- a/src/App/Resources/AppResources.id.resx +++ b/src/App/Resources/AppResources.id.resx @@ -275,6 +275,18 @@ Anda yakin ingin keluar? + + Hapus Akun + + + Apakah anda yakin ingin menghapus akun ini? + + + Akun telah ditambahkan + + + Would you like to switch to it now? + Kata Sandi Utama Label for a master password. @@ -2092,8 +2104,32 @@ One or more organization policies prevents your from exporting your personal vault. + + Tambahkan Akun + + + Tidak terkunci + + + Terkunci + + + Keluar + + + Switched to next available account + + + Akun terkunci + + + Berhasil keluar + + + Account removed successfully + - Delete Account + Hapus akun Deleting your account is permanent @@ -2102,13 +2138,13 @@ Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue? - Deleting your account + Hapus akun anda - Your account has been permanently deleted + Akun Anda telah dihapus secara permanen. - Invalid Verification Code. + Kode verifikasi tidak valid. Request one-time password @@ -2126,18 +2162,18 @@ Sending code - Verifying + Memverifikasi... - Resend Code + Kirim ulang kode - A verification code was sent to your email + Kode verifikasi telah dikirim ke email Anda. An error occurred while sending a verification code to your email. Please try again - Enter the verification code that was sent to your email + Masukkan kode verifikasi yang dikirim ke email anda diff --git a/src/App/Resources/AppResources.it.resx b/src/App/Resources/AppResources.it.resx index c3986951e..2642b4392 100644 --- a/src/App/Resources/AppResources.it.resx +++ b/src/App/Resources/AppResources.it.resx @@ -196,7 +196,7 @@ Inviaci un'email per ottenere aiuto o fare una segnalazione. - Inserisci il tuo PIN. + Digita il tuo PIN. Preferiti @@ -247,7 +247,7 @@ Description message for the alert when internet connection is required to continue. - Connessione ad Internet richiesta + Connessione a Internet richiesta Title for the alert when internet connection is required to continue. @@ -275,6 +275,18 @@ Sei sicuro di volerti disconnettere? + + Rimuovi account + + + Sei sicuro di voler rimuovere questo account? + + + Account già aggiunto + + + Vuoi passarci adesso? + Password principale Label for a master password. @@ -366,7 +378,7 @@ Verifica impronta - Verifica Password Principale + Verifica password principale Verifica PIN @@ -381,7 +393,7 @@ Visita il nostro sito - Visita il nostro sito per ottenere aiuto, notizie, mandarci una email e/o imparare di più su come usare Bitwarden. + Visita il nostro sito per ottenere aiuto, notizie, mandarci un'email e/o imparare di più su come usare Bitwarden. Sito web @@ -418,7 +430,7 @@ Il modo più semplice per aggiungere nuovi login alla tua cassaforte è dall'estensione dell'applicazione Bitwarden. Scopri di più sull'utilizzo dell'estensione dell'applicazione Bitwarden navigando nella schermata "Impostazioni". - Usa Bitwarden su Safari e altre app per auto-completare i tuoi login. + Usa Bitwarden su Safari e altre applicazioni per auto-completare i tuoi login. Servizio auto-completamento di Bitwarden @@ -436,7 +448,7 @@ Cambia password principale - Puoi cambiare la tua password principale solo sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito? + Puoi cambiare la tua password principale solo sulla cassaforte in linea di bitwarden.com. Vuoi visitare ora il sito? Chiudi @@ -455,7 +467,7 @@ Modifica elemento - Abilita la sincronizzazione automatica + Abilita sincronizzazione automatica Inserisci l'indirizzo email del tuo account per ricevere il suggerimento della password principale. @@ -467,7 +479,7 @@ Quasi fatto! - Abilita l'Estensione App + Abilita l'estensione dell'applicazione Su Safari, trova Bitwarden utilizzando l'icona di condivisione (suggerimento: scorri a destra sulla riga inferiore del menu). @@ -477,7 +489,7 @@ Ottieni accesso immediato alle tue password! - Sei pronto a fare il login! + Sei pronto ad accedere! I tuoi login sono ora facilmente accessibili da Safari, Chrome e altre applicazioni supportate. @@ -498,10 +510,10 @@ Impronta - Genera Password + Genera password - Ottenere il suggerimento per la password principale + Ottieni il suggerimento per la password principale Importa elementi @@ -543,7 +555,7 @@ Azione timeout cassaforte - La disconnessione rimuove tutti gli accessi alla tua cassaforte e richiede l'autenticazione online dopo il periodo di scadenza. Sei sicuro di voler utilizzare questa impostazione? + La disconnessione rimuove tutti gli accessi alla tua cassaforte e richiede l'autenticazione in linea dopo il periodo di scadenza. Sei sicuro di voler utilizzare questa impostazione? Connessione in corso... @@ -562,10 +574,10 @@ La password principale è la password che utilizzi per accedere alla tua cassaforte. È molto importante che tu non la dimentichi. Non c'è modo di recuperare questa password nel caso che tu la dimenticassi. - Indizio per la Password Principale (opzionale) + Suggerimento per la password principale (facoltativo) - Un indizio per la password principale può aiutarti a ricordarla nel caso te la dimenticassi. + Un suggerimento per la password principale può aiutarti a ricordarla nel caso la dimenticassi. La password principale deve essere lunga almeno 8 caratteri. @@ -582,7 +594,7 @@ Altre impostazioni - È necessario accedere all'app di Bitwarden principale prima di poter utilizzare l'estensione. + È necessario accedere all'applicazione principale di Bitwarden prima di poter utilizzare l'estensione. Mai @@ -607,7 +619,7 @@ Confirmation, like "Ok, I understand it" - Le opzioni predefinite sono impostate dal generatore di password dell'app principale Bitwarden. + Le opzioni predefinite sono impostate dal generatore di password dell'applicazione principale Bitwarden. Opzioni @@ -619,7 +631,7 @@ Password generata. - Generatore di Password + Generatore di password Suggerimento password @@ -644,7 +656,7 @@ Rigenera password - Ri-digita la tua password principale + Ri-digita la password principale Cerca nella cassaforte @@ -659,7 +671,7 @@ Imposta PIN - Immetti un codice PIN a 4 cifre per sbloccare l'applicazione. + Digita un codice PIN a 4 cifre per sbloccare l'applicazione. Informazioni elemento @@ -692,7 +704,7 @@ Verifica in due passaggi - L'autenticazione in due passaggi rende il tuo account più sicuro, richiedendo di verificare l'accesso con un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata o email. Può essere abilitata su bitwarden.com. Vuoi visitare il sito ora? + L'autenticazione in due passaggi rende il tuo account più sicuro, richiedendo di verificare l'accesso con un altro dispositivo come una chiave di sicurezza, applicazione di autenticazione, SMS, telefonata o email. Può essere abilitata su bitwarden.com. Vuoi visitare il sito ora? Sblocca con {0} @@ -747,7 +759,7 @@ Apri impostazioni di accessibilità - 1. Nella schermata impostazioni di accessibilità di Android, tocca "Bitwarden". + 1. Nella schermata impostazioni di accessibilità di Android, tocca "Bitwarden" nella gestione dei servizi. 2. Attiva l'interruttore e premi OK per accettare. @@ -1585,7 +1597,7 @@ Verifica biometrica - Dati biometrici + dati biometrici Sblocca con dati biometrici @@ -1653,7 +1665,7 @@ Clone an entity (verb). - Una o più policy dell'organizzazione controllano le impostazioni del tuo generatore + Una o più politiche dell'organizzazione stanno influenzando le impostazioni del tuo generatore Apri @@ -1914,7 +1926,7 @@ Sei sicuro di voler rimuovere la password? - Rimuovo la password + Rimozione password La password è stata rimossa. @@ -2040,10 +2052,10 @@ Aggiorna password principale - La tua password principale è stata recentemente modificata da un amministratore nella tua organizzazione. Per accedere alla cassaforte, devi aggiornarla ora. Procedendo sarai disconnesso dalla sessione attuale, richiedendo di effettuare nuovamente l'accesso. Le sessioni attive su altri dispositivi possono continuare a rimanere attive per un massimo di un'ora. + La tua password principale è stata recentemente modificata da un amministratore nella tua organizzazione. Per accedere alla cassaforte, aggiorna ora la password principale. Procedendo sarai disconnesso dalla sessione attuale e ti sarà richiesto di effettuare nuovamente l'accesso. Le sessioni attive su altri dispositivi potrebbero continuare a rimanere attive per un massimo di un'ora. - Aggiornamento password in corso + Aggiornamento password Attualmente non è possibile aggiornare la password @@ -2082,7 +2094,7 @@ Assicurati che il browser predefinito supporti WebAuthn e riprova. - Questa organizzazione ha una policy aziendale che ti iscriverà automaticamente al ripristino della password. Ciò permetterà agli amministratori dell'organizzazione di cambiare la tua password principale. + Questa organizzazione ha una politica aziendale che ti iscriverà automaticamente al ripristino della password. Ciò permetterà agli amministratori dell'organizzazione di cambiare la tua password principale. Le policy dell'organizzazione controllano il timeout della tua cassaforte. Il tempo massimo consentito è di $HOURS$ ore e $MINUTES$ minuti @@ -2093,6 +2105,30 @@ Una o più policy dell'organizzazione ti impediscono di esportare la tua cassaforte personale. + + Aggiungi account + + + Sbloccato + + + Bloccato + + + Disconnesso + + + Passato all'account successivo disponibile + + + Account bloccato + + + Account disconnesso correttamente + + + Account rimosso correttamente + Elimina account diff --git a/src/App/Resources/AppResources.ja.resx b/src/App/Resources/AppResources.ja.resx index c3e36c035..61a75b71d 100644 --- a/src/App/Resources/AppResources.ja.resx +++ b/src/App/Resources/AppResources.ja.resx @@ -275,6 +275,18 @@ ログアウトしてもよろしいですか? + + アカウントを削除 + + + このアカウントを削除してもよろしいですか? + + + アカウントは追加済みです + + + 今すぐ切り替えますか? + マスターパスワード Label for a master password. @@ -2092,6 +2104,30 @@ 1 つまたは複数の組織ポリシーにより、個人の保管庫をエクスポートできません。 + + アカウントを追加 + + + ロック解除済み + + + ロック済み + + + ログアウト済み + + + 次の利用可能なアカウントに切り替えました + + + アカウントをロックしました + + + ログアウトしました + + + アカウントを削除しました + アカウントの削除 diff --git a/src/App/Resources/AppResources.ka.resx b/src/App/Resources/AppResources.ka.resx index a7a073d97..15372b1ba 100644 --- a/src/App/Resources/AppResources.ka.resx +++ b/src/App/Resources/AppResources.ka.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -1150,10 +1162,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.kn.resx b/src/App/Resources/AppResources.kn.resx index ec82b3a3f..3c2dd80fe 100644 --- a/src/App/Resources/AppResources.kn.resx +++ b/src/App/Resources/AppResources.kn.resx @@ -275,6 +275,18 @@ ಲಾಗ್ ಔಟ್ ಮಾಡಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ Label for a master password. @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.ko.resx b/src/App/Resources/AppResources.ko.resx index 57328f35b..a552de8d0 100644 --- a/src/App/Resources/AppResources.ko.resx +++ b/src/App/Resources/AppResources.ko.resx @@ -275,6 +275,18 @@ 정말 로그아웃하시겠습니까? + + 계정 제거 + + + 정말로 이 계정을 삭제하시겠어요? + + + 이미 추가된 계정입니다 + + + 해당 계정으로 전환할까요? + 마스터 비밀번호 Label for a master password. @@ -2092,26 +2104,50 @@ 하나 이상의 조직 정책이 개인 보관함을 내보내는 것을 제한하고 있습니다. + + 계정 추가 + + + 잠금 해제됨 + + + 잠김 + + + 로그아웃됨 + + + 사용 가능한 다음 계정으로 전환함 + + + 계정 잠김 + + + 계정이 성공적으로 로그아웃되었습니다 + + + 계정이 성공적으로 제거되었습니다 + - Delete Account + 계정 삭제 - Deleting your account is permanent + 계정 삭제는 영구적입니다 - Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue? + 계정과 모든 관련 데이터가 지워지며 복구할 수 없습니다. 정말로 계속하실건가요? - Deleting your account + 계정을 삭제하는 중 - Your account has been permanently deleted + 계정이 영구적으로 삭제되었습니다 잘못된 검증 코드입니다. - Request one-time password + 일회용 코드(OTP) 요청하기 코드 전송 @@ -2120,24 +2156,24 @@ 전송 중 - Copy Send link on save + 저장 시에 Send 링크 복사하기 - Sending code + 코드 전송 증 - Verifying + 인증 중 - Resend Code + 코드 재전송 - A verification code was sent to your email + 인증 코드를 이메일로 보냈습니다 - An error occurred while sending a verification code to your email. Please try again + 이메일로 인증 코드를 보내는 동안 오류가 발생했습니다. 다시 시도해주세요 - Enter the verification code that was sent to your email + 이메일로 전송된 인증 코드를 입력해주세요 diff --git a/src/App/Resources/AppResources.lv.resx b/src/App/Resources/AppResources.lv.resx index 634d0ac17..7cd73cd2d 100644 --- a/src/App/Resources/AppResources.lv.resx +++ b/src/App/Resources/AppResources.lv.resx @@ -275,6 +275,18 @@ Vai tiešām izrakstīties? + + Noņemt kontu + + + Vai tiešām noņemt šo kontu? + + + Konts jau ir pievienots + + + Vai pārslēgties uz to? + Galvenā parole Label for a master password. @@ -2092,6 +2104,30 @@ Viens vai vairāki apvienības nosacījumi neļauj izgūt privātās glabātavas saturu. + + Pievienot kontu + + + Atslēgts + + + Slēgts + + + Izrakstījies + + + Pārslēdzās uz nākamo pieejamo kontu + + + Konts ir slēgts + + + Izrakstīšanās no konta bija veiksmīga + + + Konts tika veiksmīgi noņemts + Dzēst kontu diff --git a/src/App/Resources/AppResources.ml.resx b/src/App/Resources/AppResources.ml.resx index faca7a664..a205a062e 100644 --- a/src/App/Resources/AppResources.ml.resx +++ b/src/App/Resources/AppResources.ml.resx @@ -275,6 +275,18 @@ നിങ്ങൾക്ക് ലോഗ് ഔട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + പ്രാഥമിക പാസ്‌വേഡ് Label for a master password. @@ -2092,6 +2104,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.nb.resx b/src/App/Resources/AppResources.nb.resx index 03fb9e3ce..1adfcbb43 100644 --- a/src/App/Resources/AppResources.nb.resx +++ b/src/App/Resources/AppResources.nb.resx @@ -275,6 +275,18 @@ Er du sikker på at du vil logge av? + + Fjern konto + + + Er du sikker på at du vil fjerne denne kontoen? + + + Kontoen er allerede lagt til + + + Would you like to switch to it now? + Superpassord Label for a master password. @@ -537,13 +549,13 @@ Umiddelbart - Tidsavbrudd i hvelvet + Tidsavbrudd for hvelvet - Handling ved pause i hvelvet + Hendelse ved tidsavbrudd for hvelvet - Hvis du logger ut, fjerner du all tilgang til hvelvet ditt og krever online godkjenning etter tidsavbrudd. Er du sikker på at du vil bruke denne innstillingen? + Dersom du logger ut vil all tilgang til hvelvet fjernes og du må autentisere online etter tidsavbruddet. Er du sikker på at du vil bruke denne innstillingen? Logger på... @@ -738,7 +750,7 @@ This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - Når du velger et inntastingsfelt og ser Bitwardens tjeneste for automatiske utfylling, kan du trykke på den for å starte den automatisk utfyllingen. + Når du velger et inntastingsfelt og ser Bitwardens autofylltjeneste, kan du trykke på den for å starte autofylltjenesten. Trykk på denne beskjeden for å auto-utfylle en gjenstand fra ditt hvelv. @@ -762,7 +774,7 @@ Status - Den enkleste måten å legge til nye innlogginger i hvelvet ditt, er fra Bitwarden sin auto-utfyllingstjeneste. Lær mer om å bruke Bitwarden sin auto-utfyllingstjeneste ved å navigere til "Innstillinger". + Den enkleste måten å legge til nye innlogginger i hvelvet ditt, er fra Bitwarden sin autofylltjeneste. Lær mer om å bruke Bitwarden sin autofylltjeneste ved å navigere til "Innstillinger". Auto-utfylling @@ -1144,16 +1156,16 @@ Det er ingen gjenstander i denne mappen. - Det er ingen objekter i papirkurven. + Det er ingen elementer i papirkurven. Auto-utfyllingstilgjengelighetstjeneste - Bitwarden sin auto-utfyllingstjeneste bruker Android Autofill Framework for å bidra til å fylle inn innlogginger, bankkort og identifikasjonsinfo inn i andre apper på din enhet. + Bitwarden sin autofylltjeneste bruker Android Autofill Framework for å bidra til å fylle inn innlogginger, bankkort og identifikasjonsinfo inn i andre apper på din enhet. - Bruk Bitwarden sin auto-utfyllingstjeneste for å fylle ut innlogginger, bankkort og identifikasjonsinfo i andre apper. + Bruk Bitwarden sin autofylltjeneste for å fylle ut innlogginger, bankkort og identifikasjonsinfo i andre apper. Åpne auto-utfyllingsinnstillingene @@ -1187,7 +1199,7 @@ Skjult - Tilknyttet + Linket Tekst @@ -1287,7 +1299,7 @@ 1. Gå til iOS-appen «Innstillinger» - 2. Trykk på «Passord og kontoer» + 2. Trykk på "Passord og kontoer" 3. Trykk «Autoutfyll passord» @@ -1317,10 +1329,10 @@ Innlogginger - Sikre notiser + Sikre notater - Alle gjenstander + Alle elementer URI-er @@ -1331,13 +1343,13 @@ A loading message when doing an exposed password check. - Sjekk om passordet har blitt utsatt. + Sjekk om passordet har blitt lekket. - Dette passordet har blitt utsatt {0} gang(er) i et databrudd. Du burde endre det. + Dette passordet har blitt eksponert {0} gang(er) i et datalekkasjer. Du bør endre det. - Dette passordet ble ikke funnet i noen kjente databrudd. Det burde være trygt å bruke. + Dette passordet ble ikke funnet i noen kjente datalekkasjer. Det bør være trygt å bruke. Identitetsnavn @@ -1352,10 +1364,10 @@ Typer - Det er ingen passord å liste opp. + Det er ingen passord å vise - Det er ingen gjenstander å liste opp. + Det er ingen elementer å vise. Søk i samling @@ -1376,13 +1388,13 @@ Flytt opp - Annet + Diverse Eierskap - Hvem eier dette objektet? + Hvem eier dette elementet? Det er ingen samlinger å liste opp. @@ -1392,7 +1404,7 @@ ex: Item moved to Organization. - Gjenstanden har blitt delt. + Elementet har blitt delt. Du må velge minst én samling. @@ -1401,13 +1413,13 @@ Del - Del objekt + Del element Flytt til organisasjon - Det er ingen organisasjoner å liste opp. + Det er ingen organisasjoner å vise. Velg en organisasjon som du ønsker å flytte denne gjenstanden til. Flytting til en organisasjon overfører eierskap til den aktuelle organisasjonen. Du vil ikke lenger være den direkte eieren av denne varen når den er flyttet. @@ -1419,7 +1431,7 @@ Passfrase - Ordadskiller + Orddeler Tøm @@ -1430,7 +1442,7 @@ Short for "Password Generator" - Det er ingen mapper å liste opp. + Det er ingen mapper å vise Fingeravtrykksfrase @@ -1462,10 +1474,10 @@ 30 minutter - Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet. + Angi PIN-koden for å låse opp Bitwarden. PIN-innstillingene dine blir tilbakestilt hvis du noen gang logger deg ut av applikasjonen. - Logget på som {0} gjennom {1}. + Logget på som {0} på {1}. ex: Logged in as user@example.com on bitwarden.com. @@ -1482,7 +1494,7 @@ A dark color - Lyst + Lys A light color @@ -1505,7 +1517,7 @@ Clipboard is the operating system thing where you copy/paste data to on your device. - Slett automatisk kopierte verdier fra utklippstavlen. + Slett kopierte verdier fra utklippstavlen automatisk. Clipboard is the operating system thing where you copy/paste data to on your device. @@ -1532,10 +1544,10 @@ Avslutt - Er du sikker på at du vil lukke Bitwarden? + Er du sikker på at du vil avslutte Bitwarden? - You you want to require unlocking with your master password when the application is restarted? + Ønsker du å låse opp ved å bruke hovedpassordet når applikasjonen startes på nytt? Svart @@ -1545,13 +1557,13 @@ Svartelistede URI-er - URIs that are blacklisted will not offer auto-fill. The list of apps should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android". + URI-er som er svartelistet vil ikke bli fylt ut automatisk. Listen over apper bør kommasepareres. F.eks.: "https://twitter.com, androidapp://com.twitter.android". - Skru av lagreforespørsel + Deaktiver lagreforespørsel - "Lagreforespørselen" spør deg automatisk om du vil lagre nye objekter i hvelvet ditt når du skriver dem inn for første gang. + "Lagreforespørselen" spør deg automatisk om du vil lagre nye elementer i hvelvet ditt når du skriver dem inn for første gang. Ved omstart av appen @@ -1567,7 +1579,7 @@ ex. Uppercase the first character of a word. - Inkluder nummer + Inkluder siffer Last ned @@ -1576,13 +1588,13 @@ Delt - Toggle Visiblity + Filsynlighet av/på - Innloggingsøkten din har utløpt. + Økten din har utløpt. - Bruk biometri for å bekrefte. + Biometrisk verifisering Biometri @@ -1615,7 +1627,7 @@ Filformat - Skriv inn ditt superpassordet for å eksportere dine hvelvdataer. + Skriv inn hovedpassordet for å eksportere hvelvdataene. Send en verifiseringskode til e-posten din @@ -1627,13 +1639,13 @@ Bekreft din identitet for å fortsette. - Denne eksporten inneholder hvelvdataene dine i et ukryptert format. Du skal ikke lagre eller sende den eksporterte filen over usikre kanaler (for eksempel e-post). Slett den umiddelbart etter at du er ferdig med å bruke den. + Denne eksporten inneholder hvelvdataene dine i et ukryptert format. Du bør ikke lagre eller sende den eksporterte filen over usikre kanaler (for eksempel e-post). Slett den umiddelbart etter at du er ferdig med å bruke den. Denne eksporten krypterer dataene dine ved hjelp av kontoen din sin krypteringsnøkkel. Hvis du noen gang endrer krypteringsnøkkelen til kontoen din, bør du eksportere dataene igjen, ettersom du da ikke vil kunne dekryptere denne eksportfilen. - Kontokrypteringsnøkler er unike for hver Bitwarden sin brukerkonto, og du kan ikke importere en kryptert eksport til en annen konto. + Kontokrypteringsnøkler er unike for hver Bitwarden sin brukerkonto, så du kan ikke importere en kryptert eksport til en annen konto. Bekreft eksport av hvelvet @@ -1676,7 +1688,7 @@ Message shown when interacting with the server - Varen ble sendt til papirkurven. + Elementet ble sendt til papirkurven. Confirmation message after successfully soft-deleting a login @@ -1688,7 +1700,7 @@ Message shown when interacting with the server - Varen har blitt gjenopprettet. + Elementet har blitt gjenopprettet. Confirmation message after successfully restoring a soft-deleted item @@ -1704,7 +1716,7 @@ Confirmation alert message when permanently deleteing a cipher. - Vil du virkelig gjenopprette denne gjenstanden? + Vil du virkelig gjenopprette dette elementet? Confirmation alert message when restoring a soft-deleted cipher. @@ -1739,7 +1751,7 @@ Angi hovedpassord - For å fullføre innloggingen med SSO, angi et superpassord for å få tilgang til og beskytte hvelvet ditt. + For å fullføre innloggingen med SSO, angi et hovedpassord for å få tilgang til og beskytte hvelvet ditt. En eller flere av organisasjonens vilkår krever hovedpassordet ditt for å oppfylle følgende krav: @@ -1751,13 +1763,13 @@ Minste lengde på {0} - Inneholder ett eller flere store tegn + Inneholder én eller flere store bokstaver - Inneholder ett eller flere små tegn + Inneholder én eller flere små bokstaver - Inneholde ett eller flere tall + Inneholde ett eller flere siffer Inneholder ett eller flere av de følgende spesialtegn: {0} @@ -1766,7 +1778,7 @@ Ugyldig passord - Passordet oppfyller ikke organisasjonens krav. Kontroller policyinformasjonen og prøv på nytt. + Passordet oppfyller ikke organisasjonens krav. Kontroller organisasjonens vilkår og prøv på nytt. Laster @@ -1776,13 +1788,13 @@ - Bruksvilkårene og personvernerklæring er ikke godkjent. + Vilkårene for bruk og personvernerklæring er ikke akseptert. Vilkår for bruk - Retningslinjer for personvern + Personvernsretningslinjer Bitwarden trenger oppmerksomhet – Aktiver "Draw-Over" i "Auto-utfyllingstjenester" fra Bitwarden-innstillinger @@ -1824,10 +1836,10 @@ Hvis aktivert, vil tilgjengelighet vise en popup for å forsterke Autofill Service for eldre apper som ikke støtter Android Autofill Framework. - På grunn av bedrifsretningslinjer er du begrenset fra å lagre objekter til ditt personlige hvelv. Endre alternativ for eierskap til en organisasjon og velg blant tilgjengelige samlinger. + På grunn av virksomhetsvilkår er du begrenset fra å lagre objekter til ditt personlige hvelv. Endre alternativ for eierskap til en organisasjon og velg blant tilgjengelige samlinger. - En bedriftsretningslinje påvirker dine eierskapsinnstillinger. + Organisasjonsvilkår påvirker dine eierskapsinnstillinger. Send @@ -1865,10 +1877,10 @@ Dato for sletting - Sletting tid + Slettetidspunkt - Send-en vil bli slettet permanent på den angitte dato og klokkeslett. + Send-en vil bli permanent slettet på angitte dato og klokkeslett. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1888,7 +1900,7 @@ Utløpt - Maksimal antall tilganger + Maksimalt antall tilganger Hvis satt, vil brukere ikke lenger ha tilgang til dette send når maksimal antall tilgang er nådd. @@ -1898,13 +1910,13 @@ Maksimalt antall tilganger nådd - Antall nåværende tilgang + Antall nåværende tilganger Nytt passord - Eventuelt krever et passord for brukere å få tilgang til denne Send. + Valgfritt passordkrav å få tilgang til denne Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1928,18 +1940,18 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Det er ingen sendinger på kontoen din. + Det er ingen Send-er i kontoen din. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Legg til en sendt + Legg til en Send 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. Kopier lenke - Del link + Del lenke Send lenke @@ -1996,7 +2008,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - På grunn av en virksomhetsregel kan du kun slette en eksisterende Send. + På grunn av virksomhetsvilkår kan du kun slette en eksisterende Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2007,11 +2019,11 @@ Skjul min e-postadresse fra mottakere. - En eller flere av organisasjons retningslinjer påvirker generatorinnstillingene dine. + En eller flere av organisasjons vilkår påvirker generatorinnstillingene dine. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Gratis kontoer kan bare dele tekst. Et premiummedlemskap kreves for å bruke filer med Send. + Gratiskontoer kan kun dele tekst. Et premiummedlemskap kreves for å bruke filer med Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2022,13 +2034,13 @@ Forespørsel om hovedpassord på nytt - Superpassord bekreftelse + Hovedpassord bekreftelse - Denne handlingen er beskyttet, for å fortsette å skrive inn superpassordet på nytt for å verifisere din identitet. + Denne handlingen er beskyttet, for å fortsette å skrive inn hovedpassordet på nytt for å verifisere din identitet. - Bekreftelsekode kreves + Captcha kreves Captcha feilet. Prøv på nytt. @@ -2082,16 +2094,40 @@ Sørg for at standardnettleseren din støtter WebAuthn og prøv igjen. - Denne organisasjonen har en bedriftsoppsettsregel som automatisk innrullerer deg i tilbakestilling av passord. Registrering vil tillate organisasjonsadministratorer å endre hovedpassordet ditt. + Denne organisasjonen har en virksomhetsvilkår som automatisk innrullerer deg i tilbakestilling av passord. Registrering vil tillate organisasjonsadministratorer å endre hovedpassordet ditt. - Din organisasjons retningslinjer påvirker tidsavbruddet for hvelvet. Maksimalt tillatt tidsavbrudd for hvelv er {0} time(r) og {1} minutt(er) + Din organisasjons vikår påvirker tidsavbruddet for hvelvet. Maksimalt tillatt tidsavbrudd for hvelv er {0} time(r) og {1} minutt(er) Tidsavbruddet ditt for hvelvet overstiger begrensningene som er satt av organisasjonen din. - En eller flere organisasjonsoppsettsregler hindrer deg i å eksportere ditt personlige hvelv. + En eller flere organisasjonvilkår hindrer deg i å eksportere ditt personlige hvelv. + + + Legg til konto + + + Ulåst + + + Låst + + + Logget av + + + Byttet til neste tilgjengelige konto + + + Konto låst + + + Kontoen er logget av + + + Kontoen er fjernet Slett konto @@ -2136,9 +2172,9 @@ En verifiseringskode er sendt til din e-post - En feil oppstod ved sending av en verifiseringskode til e-posten. Vennligst prøv igjen + En feil oppstod ved sending av verifiseringskode til e-posten. Vennligst prøv igjen - Skriv inn bekreftelseskoden som ble sendt til din e-post + Skriv inn verifiseringskoden som ble sendt til din e-post diff --git a/src/App/Resources/AppResources.nl.resx b/src/App/Resources/AppResources.nl.resx index 06408857d..0325ec3c2 100644 --- a/src/App/Resources/AppResources.nl.resx +++ b/src/App/Resources/AppResources.nl.resx @@ -275,6 +275,18 @@ Weet je zeker dat je wilt uitloggen? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Hoofdwachtwoord Label for a master password. @@ -2092,6 +2104,30 @@ Organisatiebeleid voorkomt dat je je persoonlijke kluis exporteert. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account geblokkeerd + + + Account succesvol uitgelogd + + + Account succesvol verwijderd + Account verwijderen diff --git a/src/App/Resources/AppResources.nn.resx b/src/App/Resources/AppResources.nn.resx index a7a073d97..15372b1ba 100644 --- a/src/App/Resources/AppResources.nn.resx +++ b/src/App/Resources/AppResources.nn.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -1150,10 +1162,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.pl.resx b/src/App/Resources/AppResources.pl.resx index 458b70cb3..be4d55593 100644 --- a/src/App/Resources/AppResources.pl.resx +++ b/src/App/Resources/AppResources.pl.resx @@ -275,6 +275,18 @@ Czy na pewno chcesz się wylogować? + + Usuń konto + + + Czy na pewno chcesz usunąć to konto? + + + Konto zostało już dodane + + + Czy chcesz przełączyć się teraz? + Hasło główne Label for a master password. @@ -1150,10 +1162,10 @@ Usługa ułatwienia dostępu - Usługa autouzupełniania Bitwarden wykorzystuje funkcję Androida, aby wypełniać dane logowania, kart kredytowych i osobistych informacji w aplikacjach na Twoim urządzeniu. + Usługa autouzupełniania Bitwarden wykorzystuje funkcję Androida, aby wypełnić dane logowania w innych aplikacjach na Twoim urządzeniu. - Użyj usługi autouzupełniania Bitwarden, aby wypełniać dane logowania, kart kredytowych i informacje osobiste w innych aplikacjach. + Użyj usługi autouzupełniania Bitwarden, aby wypełnić dane logowania w innych aplikacjach. Otwórz ustawienia autouzupełniania @@ -1287,7 +1299,7 @@ 1. przejdź do Ustawień systemu iOS - 2. Dotknij "Hasła i konta" + 2. Dotknij "Hasła" 3. Dotknij opcji "Usługa autouzupełniania" @@ -2092,6 +2104,30 @@ Co najmniej jedna zasada organizacji uniemożliwia wyeksportowanie Twojego sejfu. + + Dodaj konto + + + Odblokowane + + + Zablokowane + + + Wylogowano + + + Przełączono na następne dostępne konto + + + Konto zostało zablokowane + + + Wylogowano z konta + + + Konto zostało usunięte + Usuń konto diff --git a/src/App/Resources/AppResources.pt-BR.resx b/src/App/Resources/AppResources.pt-BR.resx index 7a7ce3f8d..fd10d3f4c 100644 --- a/src/App/Resources/AppResources.pt-BR.resx +++ b/src/App/Resources/AppResources.pt-BR.resx @@ -275,6 +275,18 @@ Tem certeza que deseja sair? + + Remover conta + + + Tem certeza que deseja remover essa conta? + + + Conta já adicionada + + + Você gostaria de mudar para ela agora? + Senha Mestra Label for a master password. @@ -1150,10 +1162,10 @@ Serviço de Acessibilidade de Autopreenchimento - O serviço de autopreenchimento do Bitwarden utiliza a Estrutura de Autopreenchimento do Android para ajudar no preenchimento de credenciais, cartões de crédito, e informação de identidade em outros aplicativos do seu dispositivo. + O serviço de autopreenchimento do Bitwarden usa a Estrutura de Preenchimento Automático do Android para auxiliar no preenchimento de credenciais em outros aplicativos no seu dispositivo. - Utilize o serviço de autopreenchimento do Bitwarden para preencher automaticamente suas credenciais, cartões de crédito e informação de identidade em outros aplicativos. + Use o serviço de acessibilidade do bitwarden para preencher automaticamente suas credenciais. Abrir Configurações de Autopreenchimento @@ -2093,6 +2105,30 @@ Uma ou mais políticas da organização impedem que você exporte seu cofre pessoal. + + Adicionar conta + + + Desbloqueada + + + Bloqueada + + + Desconectada + + + Alterada para a próxima conta disponível + + + Conta Bloqueada + + + Conta desconectada com sucesso + + + Conta removida com sucesso + Excluir Conta diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 2f9d10cc6..cd18fc3ae 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -651,7 +663,7 @@ Re-type Master Password - Search vault + Search Vault Security @@ -1154,10 +1166,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1291,7 +1303,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -1364,11 +1376,15 @@ Search collection - - Search folder + + Search File Sends - - Search type + + Search Text Sends + + + Search {0} + ex: Search Logins Type @@ -1530,7 +1546,7 @@ Default (System) - Copy Notes + Copy Note Exit @@ -1849,6 +1865,9 @@ A friendly name to describe this Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. + + Text + Text @@ -1865,6 +1884,18 @@ The file you want to send. + + File type is selected. + + + File type is not selected, tap to select. + + + Text type is selected. + + + Text type is not selected, tap to select. + Deletion Date @@ -2097,6 +2128,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account @@ -2145,4 +2200,52 @@ Enter the verification code that was sent to your email + + Report crash logs + + + Help Bitwarden improve app stability by allowing crash reports. + + + Options are expanded, tap to collapse. + + + Options are collapsed, tap to expand. + + + Uppercase (A to Z) + + + Lowercase (A to Z) + + + Numbers (0 to 9) + + + Special Characters (!@#$%^&*) + + + Tap to go back + + + Password is visible, tap to hide. + + + Password is not visible, tap to show. + + + Filter items by vault + + + All Vaults + + + Vaults + + + Vault: {0} + + + All + diff --git a/src/App/Resources/AppResources.ro.resx b/src/App/Resources/AppResources.ro.resx index c618e2324..3a019ce7d 100644 --- a/src/App/Resources/AppResources.ro.resx +++ b/src/App/Resources/AppResources.ro.resx @@ -275,6 +275,18 @@ Sigur doriți să vă deconectați? + + Eliminare cont + + + Sunteți sigur că doriți să eliminați acest cont? + + + Cont deja adăugat + + + Vreți să comutați la el acum? + Parolă principală Label for a master password. @@ -1150,10 +1162,10 @@ Serviciul de accesibilitate pentru auto-completare - Serviciul de auto-completare Bitwarden folosește Android Autofill Framework pentru a asista completarea informațiilor de autentificare și a celor asociate cardurilor și identităților în alte aplicații de pe dispozitivul dvs. + Serviciul de autocompletare Bitwarden folosește Android Autofill Framework pentru a ajuta la completarea informațiilor de autentificare în alte aplicații de pe dispozitivul dumneavoastră. - Serviciul de auto-completare Bitwarden se folosește pentru a completa informațiile de autentificare și cele asociate cardurilor și identităților în alte aplicații. + Folosiți serviciul de autocompletare Bitwarden pentru a completa informațiile de autentificare în alte aplicații. Setări de auto-completare @@ -1287,7 +1299,7 @@ 1. Accesați Setările iOS - 2. Selectați "Parole și Conturi" + 2. Selectați „Parole” 3. Selectați "Auto-completare parole" @@ -2092,6 +2104,30 @@ Una sau mai multe politici ale organizației vă împiedică să exportați seiful personal. + + Adăugare cont + + + Deblocat + + + Blocat + + + Deconectat + + + Comutat la următorul cont disponibil + + + Cont blocat + + + Contul s-a deconectat cu succes + + + Cont eliminat cu succes + Ștergere cont diff --git a/src/App/Resources/AppResources.ru.resx b/src/App/Resources/AppResources.ru.resx index 65d0a22f0..ed1206497 100644 --- a/src/App/Resources/AppResources.ru.resx +++ b/src/App/Resources/AppResources.ru.resx @@ -275,6 +275,18 @@ Вы действительно хотите выйти? + + Удалить учетную запись + + + Вы действительно хотите удалить эту учетную запись? + + + Учетная запись уже добавлена + + + Хотите переключиться на нее? + Мастер-пароль Label for a master password. @@ -1287,7 +1299,7 @@ 1. Перейдите в 'Настройки' iOS - 2. Нажмите 'Пароли и учетные записи' + 2. Нажмите 'Пароли' 3. Нажмите 'Автозаполнение паролей' @@ -2003,7 +2015,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Скрыть мой адрес электронной почты от получателей. + Скрыть мой адрес email от получателей На параметры Send влияют одна или несколько политик организации. @@ -2092,6 +2104,30 @@ Экспорт вашего личного хранилища запрещен одной или несколькими политиками организации. + + Добавить учетную запись + + + Разблокировано + + + Заблокировано + + + Вы вышли из учетной записи + + + Переключено на следующую доступную учетную запись + + + Учетная запись заблокирована + + + Вы успешно вышли из учетной записи + + + Учетная запись успешно удалена + Удалить аккаунт diff --git a/src/App/Resources/AppResources.si.resx b/src/App/Resources/AppResources.si.resx index ffbf5d0fe..1628dd12e 100644 --- a/src/App/Resources/AppResources.si.resx +++ b/src/App/Resources/AppResources.si.resx @@ -275,6 +275,18 @@ ඔබට නික්මෙන්න අවශ්‍ය බව විශ්වාසද? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -1150,10 +1162,10 @@ Auto-fill Accessibility Service - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.sk.resx b/src/App/Resources/AppResources.sk.resx index 2ca76e274..068ca8160 100644 --- a/src/App/Resources/AppResources.sk.resx +++ b/src/App/Resources/AppResources.sk.resx @@ -275,6 +275,18 @@ Naozaj sa chcete odhlásiť? + + Odstrániť účet + + + Naozaj chcete odstrániť tento účet? + + + Účet je už pridaný + + + Chceli by ste naň prejsť teraz? + Hlavné heslo Label for a master password. @@ -1153,7 +1165,7 @@ Bitwarden služba automatického vypĺňania využíva Android Autofill Framework pre zjednodušenie vypĺňania prihlasovacích údajov, kreditných kariet a identít v aplikáciách vo vašom zariadení. - Použite službu automatického vypĺňania Bitwardenu na vypĺňanie prihlasovacích údajov, kreditných kariet a informácii identít do iných aplikácií. + Použite službu automatického vypĺňania Bitwardenu na vypĺňanie prihlasovacích údajov do iných aplikácií. Otvoriť nastavenia automatického vypĺňania @@ -1287,7 +1299,7 @@ 1. Prejdite do aplikácie "Nastavenia" pre iOS - 2. Ťuknite na položku "Heslá & účty" + 2. Ťuknite na položku "Heslá" 3. Klepnite na možnosť "Automaticky vypĺňať heslá" @@ -1416,7 +1428,7 @@ Počet slov - Heslo + Prístupová fráza Oddeľovač slov @@ -1923,7 +1935,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Vypnúť tento Send, aby k nemu nikto nemal prístup. + Vypnúť tento Send, aby k nemu nikto nemal prístup 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1991,7 +2003,7 @@ Vlastné - Po uložení zdieľať tento Send. + Po uložení zdieľať tento Send 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2003,7 +2015,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Skryť moju emailovú adresu pred príjemcami. + Skryť moju emailovú adresu pred príjemcami Jedno alebo viac pravidiel organizácie ovplyvňujú vaše možnosti funkcie Send. @@ -2092,6 +2104,30 @@ Jedna alebo viacero zásad organizácie vám bráni exportovať váš osobný trezor. + + Pridať účet + + + Odomknutý + + + Zamknutý + + + Odhlásený + + + Prepnuté na ďalší dostupný účet + + + Zamknutý účet + + + Účet bol úspešne odhlásený + + + Účet bol úspešne odstránený + Odstrániť účet diff --git a/src/App/Resources/AppResources.sl.resx b/src/App/Resources/AppResources.sl.resx index 2c97e2782..2e8ffa54a 100644 --- a/src/App/Resources/AppResources.sl.resx +++ b/src/App/Resources/AppResources.sl.resx @@ -275,6 +275,18 @@ Ste prepričani, da se želite odjaviti? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Glavno geslo Label for a master password. @@ -1150,10 +1162,10 @@ Storitev za samodejno izpolnjevanje - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. + Use the Bitwarden auto-fill service to fill login information into other apps. Nastavitve samodejnega izpolnjevanja @@ -1287,7 +1299,7 @@ 1. Pojdite v nastavitve iOS aplikacije - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Izbriši račun diff --git a/src/App/Resources/AppResources.sr.resx b/src/App/Resources/AppResources.sr.resx index 1ea815d1e..6dfa01853 100644 --- a/src/App/Resources/AppResources.sr.resx +++ b/src/App/Resources/AppResources.sr.resx @@ -275,6 +275,18 @@ Заиста желите да се одјавите? + + Уклони налог + + + Да ли сте сигурни да желите да уклоните овај налог? + + + Налог је већ додат + + + Да ли желите да га отворите сада? + Главна Лозинка Label for a master password. @@ -2094,6 +2106,30 @@ Једна или више полиса ваше организације вас спречава да извезете ваш сеф. + + Додај налог + + + Откључано + + + Закључано + + + Одјављено + + + Пребацили сте се на следећи доступни налог + + + Налог закључан + + + Успешно одјављивање + + + Налог je успешно уклоњен + Избриши Налог @@ -2113,7 +2149,7 @@ Неисправан верификациони код. - Request one-time password + Захтевај једнократну лозинку Пошаљи код @@ -2122,24 +2158,24 @@ Слање - Copy Send link on save + Копирај линк за Send након чувања - Sending code + Слање кôда - Verifying + Проверавање - Resend Code + Понови слање кôда - A verification code was sent to your email + Верификациони кôд је послат на вашу е-пошту - An error occurred while sending a verification code to your email. Please try again + Дошло је до грешке при слању верификационог кôда на вашу е-пошту. Покушајте поново - Enter the verification code that was sent to your email + Унесите верификациони кôд који је послат на Вашу е-адресу diff --git a/src/App/Resources/AppResources.sv.resx b/src/App/Resources/AppResources.sv.resx index aa01d9678..24454ee56 100644 --- a/src/App/Resources/AppResources.sv.resx +++ b/src/App/Resources/AppResources.sv.resx @@ -275,6 +275,18 @@ Är du säker på att du vill logga ut? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Huvudlösenord Label for a master password. @@ -430,7 +442,7 @@ Ändra e-postadress - You can change your email address on the bitwarden.com web vault. Do you want to visit the website now? + Du kan ändra din e-postadress på bitwardens webbvalv. Vill du besöka webbplatsen nu? Ändra huvudlösenord @@ -513,7 +525,7 @@ Massimportera dina objekt snabbt från andra lösenordshanterare. - Senaste synkning: + Senaste synkronisering: Längd @@ -682,7 +694,7 @@ Synkronisering misslyckades. - Synka valvet nu + Synkronisera valv nu Touch ID @@ -786,7 +798,7 @@ Du letar efter en inloggning som kan automatiskt fyllas i för "{0}". - Learn About Organizations + Lär dig om organisationer Kan inte öppna appen "{0}". @@ -1150,10 +1162,10 @@ Tillgänglighetstjänst för automatisk ifyllnad - Bitwardens tillgänglighetstjänst för automatisk ifyllnad använder Android Autofill Framework för att assistera med att fylla i inloggningar, kreditkort och identitetsinformation inuti andra appar på din enhet. + The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. - Använd Bitwardens tillgänglighetstjänst för att automatiskt fylla i inloggningar, kreditkort och identitetsinformation inuti andra appar. + Use the Bitwarden auto-fill service to fill login information into other apps. Öppna inställningar för automatisk ifyllnad @@ -1388,7 +1400,7 @@ Det finns inga samlingar att visa. - {0} moved to {1}. + {0} flyttades till {1}. ex: Item moved to Organization. @@ -1404,13 +1416,13 @@ Dela objekt - Move to Organization + Flytta till organisation Inga organisationer att lista. - Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved. + Välj en organisation som du vill flytta detta objektet till. Flytt till en organisation överför ägandet av objektet till den organisationen. Du kommer inte längre att vara direkt ägare till detta objekt när det har flyttats. Antal ord @@ -1441,7 +1453,7 @@ A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing. - Bitwarden allows you to share your vault items with others by using an organization account. Would you like to visit the bitwarden.com website to learn more? + Bitwarden gör det möjligt för dig att dela ditt valvs objekt med andra genom att använda ett organisationskonto. Vill du besöka bitwarden.com för att lära dig mer? Exportera valv @@ -1456,7 +1468,7 @@ Lås upp - Unlock Vault + Lås upp valv 30 minuter @@ -1838,11 +1850,11 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Försändelser + Sends 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Ett användarvänligt namn för att beskriva denna försändelse. + A friendly name to describe this Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1852,7 +1864,7 @@ Texten du vill skicka. - Dölj texten som standard när försändelsen öppnas + When accessing the Send, hide the text by default 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1868,7 +1880,7 @@ Raderingstid - Försändelsen kommer att raderas permanent på angivet datum och tid. + The Send will be permanently deleted on the specified date and time. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1881,7 +1893,7 @@ Utgångstid - Om angivet kommer åtkomst till denna försändelse upphöra på på angivet datum och tid. + If set, access to this Send will expire on the specified date and time. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1891,7 +1903,7 @@ Maximalt antal åtkomster - Om angivet kommer användare inte längre komma åt denna försändelse när den maximala antalet åtkomster har uppnåtts. + If set, users will no longer be able to access this Send once the maximum access count is reached. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1904,7 +1916,7 @@ Nytt lösenord - Kräv ett lösenord från användare innan de kommer åt denna försändelse. + Optionally require a password for users to access this Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1924,7 +1936,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Inaktivera denna försändelse så att ingen kan komma åt den. + Disable this Send so that no one can access it 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1942,11 +1954,11 @@ Dela länk - Försändelselänk + Send link 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Sök försändelser + Search Sends 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1966,7 +1978,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Send updated. + Send uppdaterad. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1992,11 +2004,11 @@ Anpassad - Dela detta den här försändelsen när den har sparats. + Share this Send upon save 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - På grund av en företagspolicy kan du bara radera befintliga försändelser. + Due to an enterprise policy, you are only able to delete an existing Send. 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2028,34 +2040,34 @@ Denna åtgärd är skyddad. För att fortsätta, vänligen verifiera din identitet genom att ange ditt huvudlösenord. - Captcha Required + CAPTCHA krävs Captcha misslyckades. Vänligen försök igen. - Updated Master Password + Huvudlösenord uppdaterades - Update Master Password + Uppdatera huvudlösenord - Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour. + Ditt huvudlösenord ändrades nyligen av en administratör i din organisation. För att få tillgång till valvet måste du uppdatera ditt huvudlösenord nu. Om du fortsätter kommer du att loggas ut från din nuvarande session, vilket kräver att du loggar in igen. Aktiva sessioner på andra enheter kan komma att vara aktiva i upp till en timme. - Updating Password + Uppdaterar lösenord - Currently unable to update password + Det går för närvarande inte att uppdatera lösenordet - Remove Master Password + Ta bort huvudlösenord - {0} is using SSO with customer-managed encryption. Continuing will remove your Master Password from your account and require SSO to login. + {0} använder SSO med kundhanterad kryptering. Fortsätter du så kommer ditt huvudlösenord att tas bort från ditt konto och kräva SSO för att logga in. - If you do not want to remove your Master Password, you may leave this organization. + Om du inte vill ta bort ditt huvudlösenord kan du lämna denna organisation. Lämna organisation @@ -2067,52 +2079,76 @@ FIDO2 WebAuthn - To continue, have your FIDO2 WebAuthn enabled security key ready, then follow the instructions after clicking 'Authenticate WebAuthn' on the next screen. + För att fortsätta, ha din FIDO2 WebAuthn aktiverad säkerhetsnyckel redo, följ sedan instruktionerna efter att du klickat på "Authenticate WebAuthn" på nästa skärm. - Authentication using FIDO2 WebAuthn, you can authenticate using an external security key. + Autentisering med FIDO2 WebAuthn, du kan autentisera med en extern säkerhetsnyckel. Autentisera WebAuthn - Return to App + Återvänd till app - Please make sure your default browser supports WebAuthn and try again. + Kontrollera att din standardwebbläsare stöder WebAuthn och försök igen. - This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password. + Denna organisation har en företagspolicy som automatiskt registrerar dig för lösenordsåterställning. Deltagandet gör det möjligt för organisationsadministratörer att ändra ditt huvudlösenord. - Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is {0} hour(s) and {1} minute(s) + Dina organisationsprinciper påverkar ditt valvs tid för timeout. Maximal tillåten tid innan timeout är {0} timme(ar) och {1} minut(er) - Your vault timeout exceeds the restrictions set by your organization. + Ditt valvs tid för timeout överskrider de begränsningar som fastställts av din organisation. - One or more organization policies prevents your from exporting your personal vault. + En eller flera organisationsprinciper hindrar dig från att exportera ditt personliga valv. + + + Lägg till konto + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully Radera konto - Deleting your account is permanent + Radering av ditt konto är permanent Ditt konto och all associerad data raderas och kan inte återställas. Är du säker på att du vill fortsätta? - Deleting your account + Raderar ditt konto - Your account has been permanently deleted + Ditt konto har raderats permanent. Ogiltig verifieringskod. - Request one-time password + Begär engångslösenord Skicka kod @@ -2121,7 +2157,7 @@ Skickar - Copy Send link on save + Kopiera Send länk vid sparning Skickar kod @@ -2130,13 +2166,13 @@ Verifierar - Resend Code + Skicka koden igen En verifieringskod skickades till din e-postadress - An error occurred while sending a verification code to your email. Please try again + Ett fel inträffade när en verifieringskod skulle skickas till din e-post. Försök igen Ange verifieringskoden som skickades till din e-postadress diff --git a/src/App/Resources/AppResources.ta.resx b/src/App/Resources/AppResources.ta.resx index 4421fe253..fc1834bd9 100644 --- a/src/App/Resources/AppResources.ta.resx +++ b/src/App/Resources/AppResources.ta.resx @@ -275,6 +275,18 @@ நிச்சயமாக வெளியேற விரும்புகிறீரா? + + கணக்கை நீக்கு + + + இக்கணக்கை நிச்சயமாக நீக்க வேண்டுமா? + + + கணக்கு ஏற்கனவே சேர்க்கப்பட்டது + + + அதற்கு இப்போது நிலைமாற விரும்புகிறீரா? + பிரதான கடவுச்சொல் Label for a master password. @@ -1151,10 +1163,10 @@ தன்னிரப்பி அணுகல்தன்மை சேவை - Bitwarden தன்னிரப்பி சேவை உள்நுழைவுகள், கடனட்டைகள், மற்றும் அடையாள தகவலை உம் சாதனத்திலிருக்கும் பிற செயலிகளினுள்ளே நிரப்புதலில் உதவ Android தன்னிரப்பி சட்டகத்தை பயன்படுத்துகிறது. + Bitwarden தன்னிரப்பி சேவை உள்நுழைவு தகவலை உம் சாதனத்திலிருக்கும் பிற செயலிகளினுள்ளே நிரப்ப உதவ Android தன்னிரப்பி சட்டகத்தைப் பயன்படுத்துகிறது. - உள்நுழைவுகள், கடனட்டைகள், மற்றும் அடையாள தகவலை பிற செயலிகளினுள்ளே நிரப்ப Bitwarden தன்னிரப்பி சேவையை பயன்படுத்து. + பிற செயலிகளினுள்ளே உள்நுழைவு தகவலை நிரப்ப Bitwarden தன்னிரப்பி சேவை பயன்படுத்து. தன்னிரப்பல் அமைவுகளைத் திற @@ -1288,7 +1300,7 @@ ௧. iOS "அமைவுகள்" செயலிக்குச் செல் - 2. "கடவுச்சொற்கள் & கணக்குகள்"ஐத் தட்டு + 2. "கடவுச்சொற்கள்"ஐத் தட்டு 3. "கடவுச்சொற்கள் தன்னிரப்பல்"ஐத் தட்டு @@ -1558,7 +1570,7 @@ செயலி மறுதொடக்கத்தில் - Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen. + மற்ற வலைத்தளங்கள் மற்றும் செயலிகளிலிருந்து உம் Bitwarden பெட்டகத்தைப் பாதுகாப்பாக அணுகுவதைத் தன்னிரப்பி எளிதாக்கும். நீங்கள் Bitwardenற்கு தன்னிரப்பி சேவையை இயக்கவில்லை போலும். "அமைவுகள்" திரையில் Bitwardenற்கு தன்னிரப்பியை இயக்குக. உமது தோற்ற மாற்றங்கள் செயலி மறுதுவக்கும்போது செயலாகும். @@ -1725,10 +1737,10 @@ கீழ் இழுக்கும் சைகையுடன் பெட்டகத்தை ஒத்திசைத்தல். - Enterprise Single Sign-On + முனைவகம் ஒற்றை உள்நுழைவு(SSO) - Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin. + உம் நிறுவனத்தின் ஒற்றை உள்நுழைவு புறையத்துடன் விரைவாக உள்நுழை. தொடங்க உம் நிறுவன அடையாளங்காட்டியை உள்ளிடவும். நிறுவன அடையாளங்காட்டி @@ -1794,22 +1806,22 @@ உள்ளக தன்னிரப்பி பயன்படுத்து - Use inline autofill if your selected IME (keyboard) supports it. If your configuration is not supported (or this option is disabled), the default Autofill overlay will be used. + உம் தேர்ந்தெடுத்த IME (விசைப்பலகை) ஆதரித்தால் உள்வரி தன்னிரப்பி பயன்படுத்துக. உம் உள்ளமைவு ஆதரிக்கப்படவில்லையெனில் (அ இவ்விருப்பம் முடங்கியிருப்பின்), இயல்பிருப்பு தன்னிரப்பி மேலடுக்கு பயன்படுத்தப்படும். அணுகல்தன்மை பயன்படுத்து - Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. When enabled, we'll display a popup when login fields are selected. + செயலிகளிலும் இணையம் முழுதுமுள்ள உமது உள்நுழைவுகளைத் தன்னிரப்ப Bitwarden அணுகல்தன்மை சேவை பயன்படுத்து. இயக்கப்பட்டால், உள்நுழைவு புலங்கள் தேர்ந்தெடுக்கப்படும்போது நாங்கள் ஒரு popup காட்டுவோம். - Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. (Requires Draw-Over to be enabled as well) + செயலிகளிலும் இணையம் முழுதுமுள்ள உமது உள்நுழைவுகளைத் தன்னிரப்ப Bitwarden அணுகல்தன்மை சேவை பயன்படுத்து. (மேலே-வரைதலும் இயங்க வேண்டும்) - Use the Bitwarden Accessibility Service to use the Autofill Quick-Action Tile, and/or show a popup using Draw-Over (if enabled). + தன்னிரப்பு விரைவுச்செயல் ஓடு பயன்படுத்த மற்றும்/அல்லது மேலே-வரைதல்(இயங்கினால்) கொண்டு popup காட்ட Bitwarden அணுகல்தன்மை சேவை பயன்படுத்து. - Required to use the Autofill Quick-Action Tile, or to augment the Autofill Service by using Draw-Over (if enabled). + தன்னிரப்பு விரைவுச்செயல் ஓடு பயன்படுத்தவோ மேலே-வரைதல்(இயங்கினால்) கொண்டு தன்னிரப்பிச் சேவையை ஆதரவுமிகுதியாக்கவோ தேவைப்படுகிறது. மேலே-வரைதல் பயன்படுத்து @@ -1818,13 +1830,13 @@ இயக்கினால், உள்நுழைவு புலங்கள் தேர்ந்தெடுக்கப்படும்போது ஒரு popup காண்பிக்க Bitwarden அணுகல்தன்மை சேவையை அனுமதிக்கிறது. - If enabled, the Bitwarden Accessibility Service will display a popup when login fields are selected to assist with auto-filling your logins. + இயக்கினால், உள்நுழைவு புலங்கள் தேர்ந்தெடுக்கப்படும்போது உமது உள்நுழைவுகளைத் தன்னிரப்ப உதவ Bitwarden அணுகல்தன்மை சேவை ஒரு popup காண்பிக்கும். - If enabled, accessibility will show a popup to augment the Autofill Service for older apps that don't support the Android Autofill Framework. + இயக்கினால், Android தன்னிரப்பி சட்டகத்தை ஆதரிக்காப் பழைய செயலிகளுக்காகத் தன்னிரப்பி சேவையை ஆதரவுமிகுதியாக்க, அணுகல்தன்மை ஒரு popup காட்டும். - Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections. + முனைவகக் கொள்கை காரணமாக, உருப்படிகளை உம் சொந்த பெட்டகத்தில் சேமிப்பதிலிருந்து கட்டுப்படுத்தப்படுகிறீர். உரிமை விருப்பத்தை நிறுவனத்திற்கு மாற்றிக் கிடைக்கும் தொகுப்புகளிலிருந்து தேர்வுசெய்க. ஒரு நிறுவன கொள்கை உம் உரிமை விருப்பங்களைப் பாதிக்கிறது. @@ -1924,7 +1936,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - இந்த Send-ஐ முடக்குக எனவே யாரும் இதை அணுக இயலாது. + இந்த Send-ஐ முடக்குக எனவே யாருமிதை அணுகவியலாது 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1992,7 +2004,7 @@ தனிபயன் - சேமிப்பின்மீது இந்த Send ஐ பகிர். + சேமிக்கையில் இந்த Send ஐப் பகிர் 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2004,7 +2016,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - பெருநர்களிடமிருந்து என் மின்னஞ்சல் முகவரியை மறை. + பெறுநர்களிடமிருந்து என் மின்னஞ்சல் முகவரியை மறை ஒன்று அ மேற்பட்ட நிறுவன கொள்கைகள் உமது Send தெரிவுகளை பாதிக்கிறது. @@ -2034,7 +2046,7 @@ கேப்ட்ச்சா தோல்வி. மீண்டும் முயலவும். - Updated Master Password + புதுப்பிக்கப்பட்ட பிரதான கடவுச்சொல் பிரதான கடவுச்சொல்லைப் புதுப்பி @@ -2082,7 +2094,7 @@ தயவுசெய்து உம் இயல்புநிலை உலாவி WebAuthn-ஐ ஆதரிக்குமென்பதை உறுதிப்படுத்தி மீண்டும் முயலவும். - This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password. + உம்மைத் தானாகக் கடவுச்சொல் அகரமாக்கலில் சேர்க்கும் முனைவகக் கொள்கை இந்நிறுவனத்திடமுள்ளது. சேர்க்கையானது உம் பிரதான கடவுச்சொல்லை நிறுவன நிர்வாகிகள் மாற்ற அனுமதிக்கும். உம் நிறுவன கொள்கைகள் உம் பெட்டக நேரமுடிவைப் பாதிக்கிறது. அனுமதிக்கப்பட்ட அதிகபட்ச பெட்டக நேரமுடிவு {0} மணிநேரம் மற்றும் {1} நிமிடம(கள்) ஆகும் @@ -2093,6 +2105,30 @@ ஒன்று அ மேற்பட்ட நிறுவன கொள்கைகள் உம் சொந்த பெட்டகத்தை ஏற்றுமதிசெய்வதைத் தவிர்க்கிறது. + + கணக்கைச் சேர் + + + பூட்டவிழ்க்கப்பட்டது + + + பூட்டப்பட்டது + + + விடுபதியப்பட்டது + + + அடுத்து கிடைத்த கணக்கிற்கு நிலைமாறியது + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + கணக்கை அழி diff --git a/src/App/Resources/AppResources.th.resx b/src/App/Resources/AppResources.th.resx index 6920043a5..ba1d2c1d0 100644 --- a/src/App/Resources/AppResources.th.resx +++ b/src/App/Resources/AppResources.th.resx @@ -275,6 +275,18 @@ คุณแน่ใจว่าต้องการออกจากระบบ + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + รหัสผ่านหลัก Label for a master password. @@ -1287,7 +1299,7 @@ 1. Go to the iOS "Settings" app - 2. Tap "Passwords & Accounts" + 2. Tap "Passwords" 3. Tap "AutoFill Passwords" @@ -2093,6 +2105,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.tr.resx b/src/App/Resources/AppResources.tr.resx index 5d4715b9a..fa71266d4 100644 --- a/src/App/Resources/AppResources.tr.resx +++ b/src/App/Resources/AppResources.tr.resx @@ -275,6 +275,18 @@ Çıkmak istediğinize emin misiniz? + + Hesabı kaldır + + + Bu hesabı kaldırmak istediğinizden emin misiniz? + + + Hesap zaten eklenmiş + + + Bu hesaba geçmek ister misiniz? + Ana parola Label for a master password. @@ -1150,10 +1162,10 @@ Otomatik doldur erişilebilirlik hizmeti - Bitwarden otomatik doldurma hizmeti; hesap, kredi kartı ve kimlik bilgilerini cihazınızdaki diğer uygulamalarda doldurmaya yardımcı olmak için Android Otomatik Doldurma Sistemi'ni kullanır. + Bitwarden otomatik doldurma hizmeti, cihazınızdaki diğer uygulamalarda hesap bilgilerini doldurmak için Android Otomatik Doldurma Sistemi'ni kullanır. - Diğer uygulamalarda hesap, kredi kartı ve kimlik bilgilerinizi doldurmak için Bitwarden otomatik doldurma hizmetini kullanabilirsiniz. + Diğer uygulamalarda hesap bilgilerinizi doldurmak için Bitwarden otomatik doldurma hizmetini kullanabilirsiniz. Otomatik doldurma ayarlarını aç @@ -1923,7 +1935,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Kimsenin erişememesi için bu Send'i devre dışı bırak. + Kimsenin erişememesi için bu Send'i devre dışı bırak 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1991,7 +2003,7 @@ Özel - Bu Send'i kaydettikten sonra paylaş. + Bu Send'i kaydettikten sonra paylaş 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2003,7 +2015,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - E-posta adresimi alıcılardan gizle. + E-posta adresimi alıcılardan gizle Bir veya daha fazla kuruluş ilkesi Send seçeneklerinizi etkiliyor. @@ -2092,6 +2104,30 @@ Bir veya daha fazla kuruluş ilkesi, kişisel kasanızı dışa aktarmanızı engelliyor. + + Hesap ekle + + + Kilitli değil + + + Kilitli + + + Çıkış yapıldı + + + Bir sonraki hesaba geçildi + + + Hesap kilitlendi + + + Başarıyla çıkış yapıldı + + + Hesap başarıyla kaldırıldı + Hesabı sil @@ -2111,7 +2147,7 @@ Doğrulama kodu geçersiz. - Request one-time password + Tek kullanımlık parola iste Kod gönder @@ -2120,24 +2156,24 @@ Gönderiliyor - Copy Send link on save + Kaydederken Send bağlantısını kopyala - Sending code + Kod gönderiliyor - Verifying + Doğrulanıyor - Resend Code + Kodu yeniden gönder - A verification code was sent to your email + E-posta adresinize doğrulama kodu gönderildi - An error occurred while sending a verification code to your email. Please try again + E-posta adresinize doğrulama kodu gönderilirken bir sorun oluştu. Lütfen daha sonra tekrar deneyin - Enter the verification code that was sent to your email + E-posta adresinize gönderilen doğrulama kodunu girin diff --git a/src/App/Resources/AppResources.uk.resx b/src/App/Resources/AppResources.uk.resx index ba3a75937..c072b6c95 100644 --- a/src/App/Resources/AppResources.uk.resx +++ b/src/App/Resources/AppResources.uk.resx @@ -275,6 +275,18 @@ Ви дійсно хочете вийти? + + Вилучити обліковий запис + + + Ви дійсно хочете вилучити цей обліковий запис? + + + Обліковий запис вже додано + + + Бажаєте перемкнутися на нього зараз? + Головний пароль Label for a master password. @@ -1150,10 +1162,10 @@ Служба спеціальних можливостей автозаповнення - Служба автозаповнення Bitwarden використовує Android Autofill Framework для заповнення паролів, кредитних карток та особистої інформації в інших програмах на вашому пристрої. + Служба автозаповнення Bitwarden використовує Android Autofill Framework для введення облікових даних в інших програмах на вашому пристрої. - Використовуйте службу автозаповнення Bitwarden, щоб автоматично вводити паролі, кредитні картки та особисту інформацію в інших програмах. + Використовуйте службу автозаповнення Bitwarden, щоб автоматично вводити облікові дані в інших програмах. Відкрити налаштування автозаповнення @@ -1923,7 +1935,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Деактивувати це відправлення для скасування доступу до нього. + Деактивувати це відправлення для скасування доступу до нього 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1991,7 +2003,7 @@ Спеціальний - Поділитися цим відправленням після збереження. + Поділитися цим відправленням після збереження 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2003,7 +2015,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Приховувати мою адресу електронної пошти від отримувачів. + Приховувати мою адресу електронної пошти від отримувачів На параметри відправлень впливають одна чи декілька політик організації. @@ -2092,6 +2104,30 @@ Одна чи декілька організаційних політик не дозволяють вам експортувати особисте сховище. + + Додати обліковий запис + + + Розблоковано + + + Заблоковано + + + Ви вийшли з системи + + + Ви перемкнулися на інший доступний обліковий запис + + + Обліковий запис заблоковано + + + Ви успішно вийшли з облікового запису + + + Обліковий запис успішно вилучено + Видалити обліковий запис diff --git a/src/App/Resources/AppResources.vi.resx b/src/App/Resources/AppResources.vi.resx index 661decc8e..9cdf2917c 100644 --- a/src/App/Resources/AppResources.vi.resx +++ b/src/App/Resources/AppResources.vi.resx @@ -275,6 +275,18 @@ Bạn có chắc chắn muốn đăng xuất không? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Mật khẩu Label for a master password. @@ -2092,6 +2104,30 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + + + Account Locked + + + Account logged out successfully + + + Account removed successfully + Delete Account diff --git a/src/App/Resources/AppResources.zh-Hant.resx b/src/App/Resources/AppResources.zh-Hant.resx index 6b8401acc..8d244049c 100644 --- a/src/App/Resources/AppResources.zh-Hant.resx +++ b/src/App/Resources/AppResources.zh-Hant.resx @@ -172,7 +172,7 @@ Message shown when interacting with the server - 確定要刪除嗎?刪除後將無法復原。 + 您確定要刪除嗎?刪除後將無法復原。 Confirmation alert message when deleteing something. @@ -219,7 +219,7 @@ 已新增資料夾。 - 資料夾已刪除。 + 已刪除資料夾。 (未分類) @@ -229,7 +229,7 @@ 資料夾 - 資料夾已更新。 + 已更新資料夾。 前往網站 @@ -243,7 +243,7 @@ Hide a secret value that is currently shown (password). - 繼續之前,請先連線至網際網路。 + 請先連線至網際網路再繼續。 Description message for the alert when internet connection is required to continue. @@ -251,10 +251,10 @@ Title for the alert when internet connection is required to continue. - 主密碼不正確,請重試。 + 主密碼不正確,請再試一次。 - PIN 碼不正確,請重試。 + PIN 碼不正確,請再試一次。 啟動 @@ -275,6 +275,18 @@ 您確定要登出嗎? + + 移除帳戶 + + + 您確定要移除這個帳戶嗎? + + + 已新增帳戶 + + + 您想現在就切換過去嗎? + 主密碼 Label for a master password. @@ -284,7 +296,7 @@ Text to define that there are more options things to see. - 我的密碼庫 + 密碼庫 The title for the vault page. @@ -326,7 +338,7 @@ Reveal a hidden value (password). - 項目已被刪除。 + 已刪除項目。 Confirmation message after successfully deleting a login. @@ -355,7 +367,7 @@ Label for a username. - {0} 欄位必須填入。 + 必須填入 {0} 欄位。 Validation message for when a form field is left blank and is required to be entered. @@ -378,7 +390,7 @@ 檢視 - 瀏覽我們的網站 + 前往我們的網站 瀏覽我們的網站以取得協助、了解新消息、傳送電子郵件給我們或瀏覽更多有關使用 Bitwarden 的秘訣。 @@ -400,7 +412,7 @@ 新增項目 - App 擴充套件 + App 延伸功能 使用 Bitwarden 無障礙服務在應用程式和網站中自動填入您的登入資料。 @@ -412,10 +424,10 @@ 避免易混淆的字元 - Bitwarden App 擴充套件 + Bitwarden App 延伸功能 - Bitwarden App 擴充套件是新增登入資料到密碼庫的最簡單方法。請至「設定」頁面了解更多關於 Bitwarden App 擴充套件的使用資訊。 + Bitwarden App 延伸功能是新增登入資料至密碼庫的最簡單方法。請至「設定」頁面了解更多關於 Bitwarden App 延伸功能的使用資訊。 在 Safari 和其他應用程式中使用 Bitwarden 自動填入登入資料。 @@ -461,13 +473,13 @@ 請輸入您的帳户電子郵件地址以接收主密碼提示。 - 重新啟用 App 擴充套件 + 重新啟用 App 延伸功能 快要完成了! - 啟用 App 擴充套件 + 啟用 App 延伸功能 在 Safari 中,使用分享圖示尋找 Bitwarden(提示:在選單最底行的右邊)。 @@ -486,7 +498,7 @@ 在 Safari 或 Chrome 中,使用分享圖示尋找 Bitwarden(提示:在分享選單的最底行的右邊)。 - 在選單中按一下 Bitwarden 圖示以啟動擴充套件。 + 在選單中按一下 Bitwarden 圖示以啟動延伸功能。 要在 Safari 或其他程式中開啟 Bitwarden ,請點選選單底部的「更多」圖示。 @@ -534,23 +546,23 @@ 4 小時 - 即時 + 立即 - 密碼庫逾時時長 + 密碼庫逾時時間 密碼庫逾時動作 - 選擇登出將會在密碼庫逾時後移除對密碼庫的所有存取權限,以及重新認證時需要連線網路。確定要使用此設定嗎? + 選擇登出將會在密碼庫逾時後移除對密碼庫的所有存取權限,若要重新驗證則需連線網路。確定要使用此設定嗎? 正在登入... Message shown when interacting with the server - 登入或建立帳戶來存取您的安全密碼庫。 + 登入或建立帳戶以存取您的安全密碼庫。 管理 @@ -559,7 +571,7 @@ 兩次填寫的主密碼不一致。 - 主密碼是您用於存取您的密碼庫的密碼。不要忘記主密碼,這一點非常重要。如果忘記了密碼,無法將其復原。 + 主密碼是用於存取密碼庫的密碼。它非常重要,請您不要忘記它。若您忘記了主密碼,沒有任何方法能將其復原。 主密碼提示(選用) @@ -582,7 +594,7 @@ 更多設定 - 您必須先登入 Bitwarden 才可使用擴充套件。 + 您必須先登入 Bitwarden 才可使用延伸功能。 永不 @@ -607,7 +619,7 @@ Confirmation, like "Ok, I understand it" - 選項預設值是通過 Bitwarden 程式的密碼產生器設定的。 + 選項預設值是透過 Bitwarden 程式的密碼產生器設定的。 選項 @@ -616,7 +628,7 @@ 其他 - 密碼已產生。 + 已產生密碼。 密碼產生器 @@ -665,7 +677,7 @@ 項目資訊 - 項目已更新。 + 已更新項目。 正在送出... @@ -692,7 +704,7 @@ 兩步驟登入 - 兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這將使您的帳戶更加安全。兩步驟登入可以在 Bitwarden 網頁版密碼庫啟用。要現在前往嗎? + 兩步驟登入需要您從其他裝置(例如安全金鑰、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 Bitwarden 網頁版密碼庫啟用。要現在前往嗎? 使用 {0} 解鎖 @@ -721,7 +733,7 @@ Screen title - 擴充套件已啟用! + 已啟用延伸功能! 圖示 @@ -750,7 +762,7 @@ 1. 在 Android 的無障礙設定畫面中,按一下服務標題下的「Bitwarden」。 - 2. 打開開關,然後按“確定”。 + 2. 開啟開關,然後按「確定」。 已停用 @@ -762,16 +774,16 @@ 狀態 - Bitwarden 自動填入服務是新增登入資料的最簡單方法。請到「設定」頁面了解更多有關使用 Bitwarden 自動填入服務的資訊 。 + Bitwarden 自動填入服務是新增登入資料的最簡單方法。請至「設定」頁面深入了解使用 Bitwarden 自動填入服務的方法 。 自動填入 - 您想自動填入還是檢視登入資料? + 您想自動填入還是檢視此項目? - 確定要自動填入此資料嗎?它與「{0}」不完全一致。 + 您確定要自動填入此項目嗎?它與「{0}」不完全一致。 相符的項目 @@ -801,7 +813,7 @@ For 2FA - 輸入已傳送至電子郵件地址 {0} 的 6 位數驗證碼。 + 輸入已傳送至電子郵件信箱 {0} 的 6 位數驗證碼。 For 2FA @@ -812,7 +824,7 @@ 此帳戶已啟用兩步驟登入,但是本裝置不支援已設定的兩步驟登入方式。請使用已支援的裝置,及/或新增可以更好地跨裝置的兩步驟登入方式(例如驗證器應用程式)。 - 復原代碼 + 復原碼 For 2FA @@ -827,10 +839,10 @@ 兩步驟登入選項 - 使用另一種兩步驟登入方式 + 使用另一種兩步驟登入方法 - 無法傳送​​驗證電子郵件。再試一次。 + 無法傳送​​驗證電子郵件。請再試一次。 For 2FA @@ -838,10 +850,10 @@ For 2FA - 要繼續的話,請將您的 YubiKey 靠在裝置的背面或者將 YubiKey 插入您裝置的 USB 連接埠,然後按下按鈕。 + 若要繼續,請將您的 YubiKey 靠在裝置的背面或者將 YubiKey 插入您裝置的 USB 連接埠,然後按下按鈕。 - YubiKey 安全鑰匙 + YubiKey 安全金鑰 "YubiKey" is the product name and should not be translated. @@ -861,7 +873,7 @@ Message shown when downloading a file - 這個附件大小為 {0} 。確定要下載到您的裝置嗎? + 這個附件大小為 {0} 。確定要下載至您的裝置嗎? The placeholder will show the file size of the attachment. Ex "25 MB" @@ -884,7 +896,7 @@ 將相機對準 QR code 。 - 掃描 QR 碼 + 掃描 QR Code 相機 @@ -896,7 +908,7 @@ 複製 TOTP - 如果您的登入資料已包含驗證器金鑰,TOTP 驗證碼會在您自動填入時自動複製到您的剪貼簿。 + 若您的登入資料已包含驗證器金鑰,TOTP 驗證碼會在您自動填入時自動複製至您的剪貼簿。 停用自動 TOTP 複製 @@ -935,7 +947,7 @@ 更新加密金鑰前不能使用此功能。 - 了解更多 + 深入了解 API 伺服器 URL @@ -947,7 +959,7 @@ 適用於進階使用者。您可以單獨指定各個服務的基礎 URL。 - 環境 URL 已儲存。 + 已儲存環境 URL。 {0} 的格式不正確。 @@ -958,7 +970,7 @@ "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - 自我託管環境 + 自我裝載環境 指定您內部部署的 Bitwarden 安裝之基礎 URL。 @@ -1036,10 +1048,10 @@ Dr - 到期月份 + 逾期月份 - 到期年份 + 逾期年份 二月 @@ -1114,10 +1126,10 @@ 地址 - 到期 + 逾期 - 停用網站圖示 + 停用網站圖示顯示功能 在您密碼庫的每個登入資料旁顯示一個可辨識的圖示。 @@ -1132,7 +1144,7 @@ 密碼庫已鎖定 - 前往我的密碼庫 + 前往密碼庫 集合 @@ -1150,13 +1162,13 @@ 自動填入無障礙服務 - Bitwarden 自動填入服務使用 Android 自動填入框架來幫助您將登入資料、信用卡和身分信息填入至裝置上的其他應用程式中。 + Bitwarden 自動填入服務使用 Android 自動填入框架來協助您將登入資訊填入至您裝置上的其他應用程式中。 - 使用 Bitwarden 自動填入服務將登入資料、信用卡和身分信息填入到其他應用程式中。 + 使用 Bitwarden 自動填入服務將登入資訊填入至其他應用程式中。 - 打開自動填入設定 + 開啟自動填入設定 Face ID @@ -1252,13 +1264,13 @@ 再試一次 - 要繼續的話,請將您的 YubiKey 靠在裝置的背面。 + 若要繼續,請將您的 YubiKey 靠在裝置的背面。 - 當應用程式不支援標準的自動填入服務時,無障礙服務或許能夠幫助使用。 + 當應用程式不支援標準的自動填入服務時,無障礙服務或許能夠幫助到您。 - 密碼已更新 + 密碼更新於 ex. Date this password was updated @@ -1266,16 +1278,16 @@ ex. Date this item was updated - 自動填入已啟用! + 已啟用自動填入! 您必須先登入 Bitwarden 應用程式,才可以使用自動填入功能。 - 登入應用程式和網站時,您可以透過鍵盤立即輕鬆存取您的登入資料。 + 登入應用程式和網站時,您可以透過鍵盤輕鬆存取您的登入資料。 - 建議您在“設定”中關閉您不再使用的其他自動填入應用程式。 + 建議您在「設定」中關閉您不再使用的其他自動填入應用程式。 您可以直接透過鍵盤存取密碼庫以快速的自動填入密碼。 @@ -1287,7 +1299,7 @@ 1. 前往 iOS 的「設定」應用程式 - 2. 點選「密碼和帳戶」 + 2. 點選「密碼」 3. 點選「自動填入密碼」 @@ -1302,7 +1314,7 @@ 密碼自動填入 - 使用 Bitwarden 密碼自動填入擴充套件是新增登入資料到密碼庫的最簡單方法。請至「設定」頁面了解更多關於 Bitwarden 密碼自動填入擴充套件的使用資訊。 + 使用 Bitwarden 密碼自動填入延伸功能是新增登入資料至密碼庫的最簡單方法。請至「設定」頁面了解更多關於 Bitwarden 密碼自動填入延伸功能的使用資訊。 無效的電子郵件地址。 @@ -1352,13 +1364,13 @@ 類型 - 沒有可顯示的密碼。 + 沒有可列出的密碼。 - 沒有可顯示的項目。 + 沒有可列出的項目。 - 搜尋收藏 + 搜尋集合 搜尋資料夾 @@ -1392,25 +1404,25 @@ ex: Item moved to Organization. - 項目已共享。 + 已共用項目。 您必須至少選擇一個集合。 - 共享 + 共用 - 分享項目 + 共用項目 移動至組織 - 沒有可顯示的組織。 + 沒有可列出的組織。 - 選擇您希望將這個項目移動到哪個組織。項目的擁有權將會轉移到該組織。一經移動,您將不再是此項目的直接擁有者。 + 選擇您希望將這個項目移動至哪個組織。項目的擁有權將會轉移至該組織。轉移之後,您將不再是此項目的直接擁有者。 字數 @@ -1430,7 +1442,7 @@ Short for "Password Generator" - 沒有可顯示的資料夾。 + 沒有可列出的資料夾。 指紋短語 @@ -1469,13 +1481,13 @@ ex: Logged in as user@example.com on bitwarden.com. - 密碼庫已鎖定。驗證主密碼以繼續。 + 密碼庫已鎖定。請驗證主密碼以繼續。 - 密碼庫已鎖定。驗證 PIN 碼以繼續。 + 密碼庫已鎖定。請驗證 PIN 碼以繼續。 - 您的密碼庫已上鎖。請驗證身份後繼續。 + 您的密碼庫已鎖定。請驗證身分以繼續。 深色 @@ -1523,7 +1535,7 @@ 變更應用程式的主題色彩。 - 預設 (系統) + 預設(系統) 複製備註 @@ -1535,7 +1547,7 @@ 您確定要結束 Bitwarden 嗎? - 當應用程式重新啟動時,是否要求用主密碼來解鎖? + 當應用程式重新啟動時,是否要求使用主密碼解鎖? 黑色 @@ -1545,22 +1557,22 @@ 黑名單 URI - 被加到黑名單的網址將不提供自動填入。應用程式串列必須以逗號分隔。範例:「https://twitter.com, androidapp://com.twitter.android」。 + 列入黑名單的網址將不提供自動填入。應用程式清單必須以逗號分隔。範例:「https://twitter.com, androidapp://com.twitter.android」。 停用儲存提示 - 當您首次輸入時,「儲存提示」會自動提示您將新項目儲存進密碼庫。 + 當您首次輸入時,「儲存提示」會自動提醒您將新項目儲存至密碼庫。 於程式重新啟動時 - 「自動填入」使其他網站及應用程式能更簡單地安全存取您的 Bitwarden 密碼庫。看起來您尚未啟用 Bitwarden 的自動填入服務。請在「設定」熒幕啟用 Bitwarden 的自動填入。 + 「自動填入」使其他網站及應用程式能更簡單地安全存取您的 Bitwarden 密碼庫。您目前尚未啟用 Bitwarden 自動填入服務。請在「設定」畫面啟用 Bitwarden 自動填入服務。 - App 重新啟動之後,您的主題變更將會被套用。 + 主題變更將於重新啟動應用程式後生效。 大寫 @@ -1576,7 +1588,7 @@ 共用 - 切換顯示 + 切換可見度 您的登入階段已過期。 @@ -1594,7 +1606,7 @@ Bitwarden 需要注意 - 請到 Bitwarden 設定中查看「自動填入無障礙服務」 - 3. 在 Android 應用程式設定介面找到 Bitwarden,進入「顯示在其他應用程式上層」選項(在「進階」下),點擊開關以開啟叠加層支持。 + 3. 在 Android 應用程式設定介面找到 Bitwarden,進入「顯示在其他應用程式上層」選項(在「進階」下),點選開關以啟用叠加層支援。 權限 @@ -1603,13 +1615,13 @@ 開啟疊加層權限設定 - 3. 在 Android 應用程式設定介面找到 Bitwarden,選擇「顯示在其他應用程式上層」(在「進階」下),並打開開關以允許叠加層。 + 3. 在 Android 應用程式設定介面找到 Bitwarden,選擇「顯示在其他應用程式上層」(在「進階」下),並開啟開關以允許叠加層。 拒絕 - 授予 + 已授予 檔案格式 @@ -1618,7 +1630,7 @@ 輸入您的主密碼以匯出密碼庫資料。 - 傳送驗證碼到您的信箱 + 傳送驗證碼至您的電子郵件信箱 驗證碼已傳送! @@ -1627,13 +1639,13 @@ 請先確認身分後再繼續。 - 此匯出包含未加密格式的密碼庫檔案。您不應將它存放或經由不安全的方式(例如電子郵件)傳送。用完後請立即將它刪除。 + 此次匯出的密碼庫檔案為未加密格式。您不應將它存放或經由不安全的方式(例如電子郵件)傳送。用完後請立即將它刪除。 - 將使用您帳戶的加密金鑰來加密匯出資料,若您更新了帳戶的加密金鑰,請重新匯出,才有辦法解密匯出的檔案。 + 將使用您帳戶的加密金鑰來加密匯出的資料,若您更新了帳戶的加密金鑰,請重新匯出,否則將無法解密匯出的檔案。 - 每個 Bitwarden 使用者帳戶的帳戶加密金鑰都不同,因此無法將加密過的匯出檔案匯入到不同帳戶中。 + 每個 Bitwarden 使用者帳戶的帳戶加密金鑰都不相同,因此無法將已加密匯出的檔案匯入至不同帳戶中。 確認匯出密碼庫 @@ -1646,24 +1658,24 @@ 匯出您的密碼庫時發生錯誤。若問題持續發生,請改用網頁版密碼庫匯出資料。 - 成功匯出密碼庫 + 已成功匯出密碼庫 複製 Clone an entity (verb). - 一個或多個組織原则正影響密碼產生器設定。 + 一個或多個組織原則正影響密碼產生器設定。 - 打開 + 開啟 Button text for an open operation (verb). 儲存附件時發生錯誤。若問題持續發生,請改用網頁版密碼庫儲存附件。 - 附件已成功儲存 + 已成功儲存附件 請到 Bitwarden 設定中開啟「自動填入無障礙服務」以使用自動填入功能。 @@ -1672,23 +1684,23 @@ 未偵測到密碼欄位 - 正在傳送到垃圾桶… + 正在傳送至垃圾桶… Message shown when interacting with the server - 項目已發送到垃圾桶。 + 已將項目傳送至垃圾桶。 Confirmation message after successfully soft-deleting a login - 恢復 + 還原 Restores an entity (verb). - 恢復中… + 正在還原... Message shown when interacting with the server - 項目已恢復。 + 已還原項目。 Confirmation message after successfully restoring a soft-deleted item @@ -1704,21 +1716,21 @@ Confirmation alert message when permanently deleteing a cipher. - 您確定要恢復此項目嗎? + 您確定要還原此項目嗎? Confirmation alert message when restoring a soft-deleted cipher. - 您確定要發送到垃圾桶嗎? + 您確定要傳送至垃圾桶嗎? Confirmation alert message when soft-deleting a cipher. - 已停用生物辨識解鎖功能,請先驗證主密碼。 + 已停用生物特徵辨識解鎖功能,請先驗證主密碼。 - 在驗證主密碼之前,用於自動填入的生物辨識功能將暫時停用。 + 在驗證主密碼之前,用於自動填入的生物特徵辨識功能將暫時停用。 - 啓用刷新時同步 + 啟用重新整理時同步 使用下拉手勢同步密碼庫。 @@ -1727,22 +1739,22 @@ 企業單一登入 - 要使用組織的單一登入入口快速登入。請輸入您組織的識別符以開始。 + 若要使用組織的單一登入入口快速登入。請先輸入您的組織識別碼。 - 組織識別符 + 組織識別碼 - 目前無法透過 SSO 登入方式登入 + 目前無法使用 SSO 登入 設定主密碼 - 要完成 SSO 登入,請設定一個主密碼以存取和保護您的密碼庫。 + 要完成 SSO 登入設定,請設定一組主密碼以存取和保護您的密碼庫。 - 一個或多個組織原则要求您的主密碼須符合下列條件: + 一個或多個組織原則要求您的主密碼須符合下列條件: 最小複雜度為 {0} @@ -1766,16 +1778,16 @@ 密碼無效 - 輸入的密碼不符合組織要求,請確認原则資訊然後再試一次。 + 輸入的密碼不符合組織要求,請確認原則資訊後再試一次。 - 載入中 + 正在載入 - 開啟此開關,代表您同意下列項目: + 開啟此開關,代表您同意下列項目: - 尚未接受服務條款與隱私權保護政策。 + 尚未接受服務條款與隱私權政策。 服務條款 @@ -1784,7 +1796,7 @@ 隱私權政策 - Bitwarden 需要注意 - 請到 Bitwarden 設定的「自動填入服務」中啓用「Draw-Over」 + Bitwarden 需要注意 - 請到 Bitwarden 設定的「自動填入服務」中啟用「Draw-Over」 自動填入服務 @@ -1793,40 +1805,40 @@ 使用內嵌式自動填入 - 若您的 IME(鍵盤)支援,將使用內嵌式自動填入。若您的設定不支援(或未啟用此選項),將使用預設的覆疊式自動填入。 + 若您的 IME(鍵盤)支援,將使用內嵌式自動填入。若您的設定不支援(或未啟用此選項),將使用預設的疊加式自動填入。 使用無障礙 - 使用 Bitwarden 無障礙服務在應用程式和網路上自動填入您的登入資料。 啟用後,當選擇登入欄位時,將顯示一個彈出視窗。 + 使用 Bitwarden 無障礙服務在應用程式和網站上自動填入您的登入資料。 啟用後,將在選擇登入欄位時顯示彈出式視窗。 - 使用 Bitwarden 無障礙服務在應用程式和網路上自動填入您的登入資料。(還需要開啟 Draw-Over) + 使用 Bitwarden 無障礙服務在應用程式和網站上自動填入您的登入資料。(需同時啟用 Draw-Over 功能) - 使用 Bitwarden 無障礙服務使用自動填入快速操作磁貼,和/或使用 Draw-Over 顯示彈出視窗(如果已開啟)。 + 使用 Bitwarden 無障礙服務來使用自動填入快速控制圖塊,和/或使用 Draw-Over 來顯示彈出式視窗(如果已開啟)。 - 需要使用自動填入快速操作磁貼或透過使用 Draw-Over(如果已開啟)來增強自動填入服務。 + 需要使用自動填入快速控制圖塊,或透過使用 Draw-Over(如果已開啟)來增強自動填入服務。 使用 Draw-Over - 啓用後,在選擇登入欄位時,將允許 Bitwarden 無障礙服務顯示一個彈出視窗。 + 啟用後,在選擇登入欄位時將允許 Bitwarden 無障礙服務顯示彈出式視窗。 - 若啓用,儅選擇登入欄位自動填入您的登入資料時,Bitwarden 無障礙服務將顯示一個彈出視窗。 + 若啟用,當選擇登入欄位自動填入您的登入資料時,Bitwarden 無障礙服務將顯示一個彈出式視窗。 - 若啓用,無障礙將顯示一個彈出視窗以增强不支援 Android 自動填入框架的舊應用程式的自動填入服務。 + 若啟用,無障礙將顯示一個彈出式視窗以增強不支援 Android 自動填入框架的舊應用程式的自動填入服務。 - 由於某個企業原则,您被限制為儲存項目到您的個人密碼庫。將擁有權變更爲組織,並從可用的集合中選擇。 + 由於某個企業原則,您被限制為儲存項目至您的個人密碼庫。將擁有權變更為組織,並從可用的集合中選擇。 - 一個組織原则正影響您的擁有權選項。 + 組織原則正在影響您的擁有權選項。 Send @@ -1841,7 +1853,7 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 用於描述此 Send 的友好名稱。 + 用於描述此 Send 的易記名稱。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1851,7 +1863,7 @@ 您想要傳送的文字。 - 存取此 Send 時,預設將隱藏文字 + 存取此 Send 時,將預設隱藏文字 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1867,11 +1879,11 @@ 刪除時間 - 此 Send 將在指定的日期和時間被永久删除。 + 此 Send 將在指定的日期和時間後被永久刪除。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 等待刪除中 + 等待刪除 逾期日期 @@ -1900,10 +1912,10 @@ 目前存取次數 - 新的密碼 + 新密碼 - 可選。使用者需提供密碼才能存取此 Send。 + 選用。使用者需提供密碼才能存取此 Send。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1916,14 +1928,14 @@ 正在移除密碼 - 密碼已移除 + 已移除密碼。 - 關於此 Send 的私人注釋。 + 關於此 Send 的私人備註。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 停用此 Send 以阻止任何人存取它。 + 停用此 Send 以阻止任何人存取 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1938,7 +1950,7 @@ 複製連結 - 分享連結 + 共用連結 Send 連結 @@ -1961,15 +1973,15 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Send 已刪除。 + 已刪除 Send。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - Send 已更新。 + 已更新 Send。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 新的 Send 已創建。 + 已建立新的 Send。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -1991,11 +2003,11 @@ 自訂 - 儲存時分享此 Send。 + 儲存時共用此 Send 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 由於某個企業原则,您只能刪除已有的 Send。 + 由於企業原則限制,您只能刪除現有的 Send。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2003,18 +2015,18 @@ 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 對收件人隱藏我的電子郵件位址。 + 對收件人隱藏我的電子郵件地址 - 一個或多個組織原则正影響您的 Send 選項。 + 一個或多個組織原則正影響您的 Send 選項。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 免費帳戶僅限於共享文字。要使用文檔 Send,需要進階會員資格。 + 免費帳戶僅限於共用文字。若想使用檔案 Send,需要進階會員資格。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. - 必須驗證您的電子郵件才能使用文檔 Send。你可以在網頁密碼庫中驗證您的電子郵件。 + 您必須驗證電子郵件才能夠使用檔案 Send。您可以在網頁版密碼庫中驗證您的電子郵件。 'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated. @@ -2024,13 +2036,13 @@ 確認主密碼 - 此動作受到保護。若要繼續,請重新輸入您的主密碼以驗證您的身份。 + 此動作受到保護。若要繼續,請重新輸入您的主密碼以驗證您的身分。 - 要求人機驗證 + 需要人機驗證 (Captcha) - 人機驗證失敗,請重試。 + 人機驗證 (Captcha) 失敗,請再試一次。 已更新主密碼 @@ -2039,22 +2051,22 @@ 更新主密碼 - 您的主密碼最近被您的組織管理者變更過。要存取密碼庫,您必須現在更新主密碼。繼續操作會登出目前的登入階段,要求您重新登入。其他裝置上使用中的登入階段可能持續最長一個小時。 + 您的主密碼最近被您的組織管理者變更過。您必須現在更新主密碼才能存取密碼庫。繼續操作會登出您目前的工作階段,並要求您重新登入帳戶。其他裝置上的活動工作階段最多會保持一個小時。 正在更新密碼 - 當前無法更新密碼 + 目前無法更新密碼 移除主密碼 - {0} 正使用客戶管理加密的 SSO。繼續操作將從您的帳戶中移除主密碼並要求 SSO 登入。 + {0} 正在使用客戶管理加密的 SSO。繼續操作將移除您帳戶的主密碼並要求 SSO 登入。 - 如果您不想移除主密碼,您可以離開這個組織。 + 若您不想移除主密碼,您可以離開此組織。 離開組織 @@ -2069,7 +2081,7 @@ 若要繼續,請準備好您已經啟用 FIDO2 WebAuthn 的安全金鑰,在下個畫面按下 [驗證 WebAuthn],接著遵循指引。 - 使用 FIDO2 WebAuthn 認證,您可以使用外部安全金鑰進行認證。 + 使用 FIDO2 WebAuthn 驗證,您可以使用外部安全金鑰進行驗證。 驗證 WebAuthn @@ -2078,28 +2090,52 @@ 返回應用程式 - 請確保您的預設瀏覽器支援 WebAuthn,然後重試。 + 請確保您的預設瀏覽器支援 WebAuthn,然後再試一次。 此組織有一個可以為您自動註冊密碼重設的企業原則。註冊後將允許組織管理員變更您的主密碼。 - 您的組織策略正在影響您的密碼庫逾時時長。密碼庫逾時時長最多可以設定到 {0} 小時,{1} 分鐘。 + 您的組織原則正在影響您的密碼庫逾時時間。密碼庫逾時時間最多可以設定到 {0} 小時 {1} 分鐘。 - 您的密碼庫逾時時長超過組織限制。 + 您的密碼庫逾時時間超過組織設定的限制。 - 一或多個組織策略不允許您匯出個人密碼庫。 + 一個或多個組織原則禁止您匯出個人密碼庫。 + + + 新增帳戶 + + + 已解鎖 + + + 已鎖定 + + + 已登出 + + + 已切換至下一個可用的帳戶 + + + 帳戶已鎖定 + + + 已成功登出帳戶 + + + 已成功移除帳戶 刪除帳戶 - 「刪除帳戶」不可逆 + 刪除帳戶是永久性的 - 您的帳戶及相關資料都會被清得一乾二凈,無法復原。是否繼續? + 您的帳戶及相關資料將被完全清除並且無法復原。您確定要繼續嗎? 正在刪除您的帳戶 @@ -2111,13 +2147,13 @@ 無效的驗證碼。 - 請求一次性密碼 + 要求一次性密碼 傳送驗證碼 - 傳送中 + 正在傳送 儲存時複製 Send 連結 @@ -2132,12 +2168,12 @@ 重新傳送驗證碼 - 驗證碼已傳送到您的電子信箱 + 已傳送驗證碼至您的電子郵件信箱 - 將驗證碼傳送至您的電子信箱時發生錯誤。請重試 + 將驗證碼傳送至您的電子郵件信箱時發生錯誤。請再試一次 - 輸入傳送到您電子信箱的驗證碼 + 輸入傳送至您電子郵件信箱的驗證碼 diff --git a/src/App/Services/MobileI18nService.cs b/src/App/Services/MobileI18nService.cs index 24c70fcf2..da86ee4fe 100644 --- a/src/App/Services/MobileI18nService.cs +++ b/src/App/Services/MobileI18nService.cs @@ -1,11 +1,11 @@ -using Bit.App.Resources; -using Bit.Core.Abstractions; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Resources; using System.Threading; +using Bit.App.Resources; +using Bit.Core.Abstractions; namespace Bit.App.Services { diff --git a/src/App/Services/MobilePasswordRepromptService.cs b/src/App/Services/MobilePasswordRepromptService.cs index 8f1b7a04c..28a8e5a86 100644 --- a/src/App/Services/MobilePasswordRepromptService.cs +++ b/src/App/Services/MobilePasswordRepromptService.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; -using System; using Bit.Core.Utilities; namespace Bit.App.Services diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs index f4e98e250..6b424dc66 100644 --- a/src/App/Services/MobilePlatformUtilsService.cs +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -14,7 +14,7 @@ using Xamarin.Forms; namespace Bit.App.Services { public class MobilePlatformUtilsService : IPlatformUtilsService - { + { private static readonly Random _random = new Random(); private const int DialogPromiseExpiration = 600000; // 10 minutes diff --git a/src/App/Services/MobileStorageService.cs b/src/App/Services/MobileStorageService.cs index d1fb15cc5..723048604 100644 --- a/src/App/Services/MobileStorageService.cs +++ b/src/App/Services/MobileStorageService.cs @@ -1,8 +1,8 @@ -using Bit.Core; -using Bit.Core.Abstractions; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core; +using Bit.Core.Abstractions; namespace Bit.App.Services { @@ -13,41 +13,28 @@ namespace Bit.App.Services private readonly HashSet _preferenceStorageKeys = new HashSet { - Constants.VaultTimeoutKey, - Constants.VaultTimeoutActionKey, - Constants.ThemeKey, - Constants.DefaultUriMatch, - Constants.DisableAutoTotpCopyKey, - Constants.DisableFaviconKey, - Constants.ClearClipboardKey, - Constants.AutofillDisableSavePromptKey, - Constants.LastActiveTimeKey, + Constants.StateVersionKey, + Constants.PreAuthEnvironmentUrlsKey, + Constants.AutofillTileAdded, + Constants.AddSitePromptShownKey, Constants.PushInitialPromptShownKey, Constants.LastFileCacheClearKey, Constants.PushLastRegistrationDateKey, Constants.PushRegisteredTokenKey, Constants.PushCurrentTokenKey, Constants.LastBuildKey, - Constants.MigratedFromV1, - Constants.MigratedFromV1AutofillPromptShown, - Constants.TriedV1Resync, Constants.ClearCiphersCacheKey, Constants.BiometricIntegrityKey, Constants.iOSAutoFillClearCiphersCacheKey, Constants.iOSAutoFillBiometricIntegrityKey, Constants.iOSExtensionClearCiphersCacheKey, Constants.iOSExtensionBiometricIntegrityKey, - Constants.EnvironmentUrlsKey, - Constants.InlineAutofillEnabledKey, - Constants.InvalidUnlockAttempts, + Constants.iOSShareExtensionClearCiphersCacheKey, + Constants.iOSShareExtensionBiometricIntegrityKey, + Constants.RememberedEmailKey, + Constants.RememberedOrgIdentifierKey, }; - private readonly HashSet _migrateToPreferences = new HashSet - { - Constants.EnvironmentUrlsKey, - }; - private readonly HashSet _haveMigratedToPreferences = new HashSet(); - public MobileStorageService( IStorageService preferenceStorageService, IStorageService liteDbStorageService) @@ -60,24 +47,9 @@ namespace Bit.App.Services { if (_preferenceStorageKeys.Contains(key)) { - var prefValue = await _preferencesStorageService.GetAsync(key); - if (prefValue != null || !_migrateToPreferences.Contains(key) || - _haveMigratedToPreferences.Contains(key)) - { - return prefValue; - } + return await _preferencesStorageService.GetAsync(key); } - var liteDbValue = await _liteDbStorageService.GetAsync(key); - if (_migrateToPreferences.Contains(key)) - { - if (liteDbValue != null) - { - await _preferencesStorageService.SaveAsync(key, liteDbValue); - await _liteDbStorageService.RemoveAsync(key); - } - _haveMigratedToPreferences.Add(key); - } - return liteDbValue; + return await _liteDbStorageService.GetAsync(key); } public Task SaveAsync(string key, T obj) diff --git a/src/App/Services/NoopPushNotificationListenerService.cs b/src/App/Services/NoopPushNotificationListenerService.cs index 7bb42b424..f15a258b5 100644 --- a/src/App/Services/NoopPushNotificationListenerService.cs +++ b/src/App/Services/NoopPushNotificationListenerService.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json.Linq; +using System.Threading.Tasks; using Bit.App.Abstractions; -using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace Bit.App.Services { diff --git a/src/App/Services/PreferencesStorageService.cs b/src/App/Services/PreferencesStorageService.cs index ae0dc6609..1fdb72434 100644 --- a/src/App/Services/PreferencesStorageService.cs +++ b/src/App/Services/PreferencesStorageService.cs @@ -1,15 +1,15 @@ -using Bit.Core.Abstractions; +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System; -using System.Threading.Tasks; namespace Bit.App.Services { public class PreferencesStorageService : IStorageService { public static string KeyFormat = "bwPreferencesStorage:{0}"; - + private readonly string _sharedName; private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs index e0d34e197..79521a7a5 100644 --- a/src/App/Services/PushNotificationListenerService.cs +++ b/src/App/Services/PushNotificationListenerService.cs @@ -1,17 +1,17 @@ -#if !FDROID +#if !FDROID +using System; using System.Diagnostics; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Bit.App.Abstractions; -using System; using Xamarin.Forms; -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using System.Threading.Tasks; -using Bit.Core.Enums; -using Bit.Core; -using Bit.Core.Models.Response; -using Bit.Core.Exceptions; namespace Bit.App.Services { @@ -21,9 +21,8 @@ namespace Bit.App.Services private bool _showNotification; private bool _resolved; - private IStorageService _storageService; private ISyncService _syncService; - private IUserService _userService; + private IStateService _stateService; private IAppIdService _appIdService; private IApiService _apiService; private IMessagingService _messagingService; @@ -64,8 +63,8 @@ namespace Bit.App.Services return; } - var myUserId = await _userService.GetUserIdAsync(); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var myUserId = await _stateService.GetActiveUserIdAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); switch (notification.Type) { case NotificationType.SyncCipherUpdate: @@ -135,7 +134,7 @@ namespace Bit.App.Services { Resolve(); Debug.WriteLine($"{TAG} - Device Registered - Token : {token}"); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); if (!isAuthenticated) { Debug.WriteLine($"{TAG} - not auth"); @@ -146,7 +145,7 @@ namespace Bit.App.Services try { #if DEBUG - await _storageService.RemoveAsync(Constants.PushInstallationRegistrationError); + await _stateService.SetPushInstallationRegistrationErrorAsync(null); #endif await _apiService.PutDeviceTokenAsync(appId, @@ -154,10 +153,10 @@ namespace Bit.App.Services Debug.WriteLine($"{TAG} Registered device with server."); - await _storageService.SaveAsync(Constants.PushLastRegistrationDateKey, DateTime.UtcNow); + await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); if (deviceType == Device.Android) { - await _storageService.SaveAsync(Constants.PushCurrentTokenKey, token); + await _stateService.SetPushCurrentTokenAsync(token); } } #if DEBUG @@ -165,11 +164,11 @@ namespace Bit.App.Services { Debug.WriteLine($"{TAG} Failed to register device."); - await _storageService.SaveAsync(Constants.PushInstallationRegistrationError, apiEx.Error?.Message); + await _stateService.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message); } catch (Exception e) { - await _storageService.SaveAsync(Constants.PushInstallationRegistrationError, e.Message); + await _stateService.SetPushInstallationRegistrationErrorAsync(e.Message); throw; } #else @@ -200,9 +199,8 @@ namespace Bit.App.Services { return; } - _storageService = ServiceContainer.Resolve("storageService"); _syncService = ServiceContainer.Resolve("syncService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _apiService = ServiceContainer.Resolve("apiService"); _messagingService = ServiceContainer.Resolve("messagingService"); diff --git a/src/App/Services/SecureStorageService.cs b/src/App/Services/SecureStorageService.cs index 5655cc17e..1c1534b68 100644 --- a/src/App/Services/SecureStorageService.cs +++ b/src/App/Services/SecureStorageService.cs @@ -1,7 +1,7 @@ -using Bit.Core.Abstractions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System.Threading.Tasks; namespace Bit.App.Services { diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml index 48edce35b..42c292738 100644 --- a/src/App/Styles/Android.xaml +++ b/src/App/Styles/Android.xaml @@ -92,7 +92,7 @@ + Value="Medium" /> + Value="Medium" /> + + diff --git a/src/App/Styles/Android.xaml.cs b/src/App/Styles/Android.xaml.cs index 166079cef..a2e9ecfd3 100644 --- a/src/App/Styles/Android.xaml.cs +++ b/src/App/Styles/Android.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml index e8a202adf..dd024bbbb 100644 --- a/src/App/Styles/Base.xaml +++ b/src/App/Styles/Base.xaml @@ -221,6 +221,14 @@ + + + + + + diff --git a/src/App/Styles/Black.xaml.cs b/src/App/Styles/Black.xaml.cs index c13d1698f..b126f1bb5 100644 --- a/src/App/Styles/Black.xaml.cs +++ b/src/App/Styles/Black.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/Dark.xaml.cs b/src/App/Styles/Dark.xaml.cs index b0bf3d949..1a4113ab6 100644 --- a/src/App/Styles/Dark.xaml.cs +++ b/src/App/Styles/Dark.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/Light.xaml.cs b/src/App/Styles/Light.xaml.cs index f7defeec7..3ddad1482 100644 --- a/src/App/Styles/Light.xaml.cs +++ b/src/App/Styles/Light.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/Nord.xaml.cs b/src/App/Styles/Nord.xaml.cs index 3ec5209f5..6baf44c3b 100644 --- a/src/App/Styles/Nord.xaml.cs +++ b/src/App/Styles/Nord.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/Variables.xaml.cs b/src/App/Styles/Variables.xaml.cs index 214142bde..31582c54d 100644 --- a/src/App/Styles/Variables.xaml.cs +++ b/src/App/Styles/Variables.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Styles/iOS.xaml b/src/App/Styles/iOS.xaml index 0ce572739..ca5201346 100644 --- a/src/App/Styles/iOS.xaml +++ b/src/App/Styles/iOS.xaml @@ -105,7 +105,7 @@ + Value="Medium" /> @@ -143,7 +143,7 @@ + Value="Medium" /> + + diff --git a/src/App/Styles/iOS.xaml.cs b/src/App/Styles/iOS.xaml.cs index 72d94ad44..a08433b33 100644 --- a/src/App/Styles/iOS.xaml.cs +++ b/src/App/Styles/iOS.xaml.cs @@ -9,4 +9,4 @@ namespace Bit.App.Styles InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index bf11e143a..454d77021 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -1,19 +1,21 @@ using System; -using Bit.App.Abstractions; -using Bit.App.Pages; -using Bit.App.Resources; -using Bit.Core; -using Bit.Core.Abstractions; -using Bit.Core.Models.View; -using Bit.Core.Utilities; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Models; +using Bit.App.Pages; +using Bit.App.Resources; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using Bit.Core.Utilities; using Newtonsoft.Json; using Xamarin.Essentials; using Xamarin.Forms; @@ -46,8 +48,8 @@ namespace Bit.App.Utilities } if (!string.IsNullOrWhiteSpace(cipher.Login.Totp)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremium = await userService.CanAccessPremiumAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + var canAccessPremium = await stateService.CanAccessPremiumAsync(); if (canAccessPremium || cipher.OrganizationUseTotp) { options.Add(AppResources.CopyTotp); @@ -195,6 +197,62 @@ namespace Bit.App.Utilities return selection; } + public static async Task AccountListOptions(ContentPage page, AccountViewCellViewModel accountViewCell) + { + var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + + var userId = accountViewCell.AccountView.UserId; + + List options; + if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) || + await vaultTimeoutService.ShouldLogOutByTimeoutAsync(userId)) + { + options = new List { AppResources.RemoveAccount }; + } + else if (await vaultTimeoutService.IsLockedAsync(userId) || + await vaultTimeoutService.ShouldLockAsync(userId)) + { + options = new List { AppResources.LogOut }; + } + else + { + options = new List { AppResources.Lock, AppResources.LogOut }; + } + + var accountSummary = accountViewCell.AccountView.Email; + if (!string.IsNullOrWhiteSpace(accountViewCell.AccountView.Hostname)) + { + accountSummary += "\n" + accountViewCell.AccountView.Hostname; + } + var selection = await page.DisplayActionSheet(accountSummary, AppResources.Cancel, null, options.ToArray()); + + if (selection == AppResources.Lock) + { + await vaultTimeoutService.LockAsync(true, true, userId); + } + else if (selection == AppResources.LogOut || selection == AppResources.RemoveAccount) + { + var title = selection == AppResources.LogOut ? AppResources.LogOut : AppResources.RemoveAccount; + var text = (selection == AppResources.LogOut ? AppResources.LogoutConfirmation + : AppResources.RemoveAccountConfirmation) + "\n\n" + accountSummary; + var confirmed = + await platformUtilsService.ShowDialogAsync(text, title, AppResources.Yes, AppResources.Cancel); + if (confirmed) + { + var stateService = ServiceContainer.Resolve("stateService"); + if (await stateService.IsActiveAccountAsync(userId)) + { + var messagingService = ServiceContainer.Resolve("messagingService"); + messagingService.Send("logout"); + return selection; + } + await LogOutAsync(userId, true); + } + } + return selection; + } + public static async Task CopySendUrlAsync(SendView send) { if (await IsSendDisabledByPolicyAsync()) @@ -221,16 +279,11 @@ namespace Bit.App.Utilities Subject = send.Name }); } - + private static string GetSendUrl(SendView send) { var environmentService = ServiceContainer.Resolve("environmentService"); - var webVaultUrl = environmentService.GetWebVaultUrl(); - if (webVaultUrl != null) - { - return webVaultUrl + "/#/send/" + send.AccessId + "/" + send.UrlB64Key; - } - return "https://send.bitwarden.com/#" + send.AccessId + "/" + send.UrlB64Key; + return environmentService.GetWebSendUrl() + send.AccessId + "/" + send.UrlB64Key; } public static async Task RemoveSendPasswordAsync(string sendId) @@ -330,33 +383,15 @@ namespace Bit.App.Utilities } public static async Task PerformUpdateTasksAsync(ISyncService syncService, - IDeviceActionService deviceActionService, IStorageService storageService) + IDeviceActionService deviceActionService, IStateService stateService) { var currentBuild = deviceActionService.GetBuildNumber(); - var lastBuild = await storageService.GetAsync(Constants.LastBuildKey); - if (lastBuild == null) - { - // Installed - var currentTimeout = await storageService.GetAsync(Constants.VaultTimeoutKey); - if (currentTimeout == null) - { - await storageService.SaveAsync(Constants.VaultTimeoutKey, 15); - } - - var currentAction = await storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (currentAction == null) - { - await storageService.SaveAsync(Constants.VaultTimeoutActionKey, "lock"); - } - } - else if (lastBuild != currentBuild) + var lastBuild = await stateService.GetLastBuildAsync(); + if (lastBuild == null || lastBuild != currentBuild) { // Updated var tasks = Task.Run(() => syncService.FullSyncAsync(true)); - } - if (lastBuild != currentBuild) - { - await storageService.SaveAsync(Constants.LastBuildKey, currentBuild); + await stateService.SetLastBuildAsync(currentBuild); return true; } return false; @@ -418,35 +453,34 @@ namespace Bit.App.Utilities public static async Task ClearPreviousPage() { - var storageService = ServiceContainer.Resolve("storageService"); - var previousPage = await storageService.GetAsync(Constants.PreviousPageKey); + var stateService = ServiceContainer.Resolve("stateService"); + var previousPage = await stateService.GetPreviousPageInfoAsync(); if (previousPage != null) { - await storageService.RemoveAsync(Constants.PreviousPageKey); + await stateService.SetPreviousPageInfoAsync(null); } return previousPage; } public static async Task IncrementInvalidUnlockAttemptsAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - var invalidUnlockAttempts = await storageService.GetAsync(Constants.InvalidUnlockAttempts); + var stateService = ServiceContainer.Resolve("stateService"); + var invalidUnlockAttempts = await stateService.GetInvalidUnlockAttemptsAsync(); invalidUnlockAttempts++; - await storageService.SaveAsync(Constants.InvalidUnlockAttempts, invalidUnlockAttempts); + await stateService.SetInvalidUnlockAttemptsAsync(invalidUnlockAttempts); return invalidUnlockAttempts; } - + public static async Task ResetInvalidUnlockAttemptsAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - await storageService.RemoveAsync(Constants.InvalidUnlockAttempts); + var stateService = ServiceContainer.Resolve("stateService"); + await stateService.SetInvalidUnlockAttemptsAsync(null); } public static async Task IsVaultTimeoutImmediateAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - - var vaultTimeoutMinutes = await storageService.GetAsync(Constants.VaultTimeoutKey); + var stateService = ServiceContainer.Resolve("stateService"); + var vaultTimeoutMinutes = await stateService.GetVaultTimeoutAsync(); if (vaultTimeoutMinutes.GetValueOrDefault(-1) == 0) { return true; @@ -463,41 +497,95 @@ namespace Bit.App.Utilities var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj)); var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte); - return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped)); + return WebUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped))); } - public static async Task LogOutAsync() + public static async Task LogOutAsync(string userId, bool userInitiated = false) + { + var stateService = ServiceContainer.Resolve("stateService"); + var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + + var isActiveAccount = await stateService.IsActiveAccountAsync(userId); + + var isAccountRemoval = await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) || + await vaultTimeoutService.ShouldLogOutByTimeoutAsync(userId); + + if (userId == null) + { + userId = await stateService.GetActiveUserIdAsync(); + } + + await stateService.LogoutAccountAsync(userId, userInitiated); + + if (isActiveAccount) + { + await ClearServiceCacheAsync(); + } + + if (!userInitiated) + { + return; + } + + // check if we switched active accounts automatically + if (isActiveAccount && await stateService.GetActiveUserIdAsync() != null) + { + var messagingService = ServiceContainer.Resolve("messagingService"); + messagingService.Send("switchedAccount"); + + var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + platformUtilsService.ShowToast("info", null, AppResources.AccountSwitchedAutomatically); + return; + } + + // check if we logged out a non-active account + if (!isActiveAccount) + { + var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + if (isAccountRemoval) + { + platformUtilsService.ShowToast("info", null, AppResources.AccountRemovedSuccessfully); + return; + } + platformUtilsService.ShowToast("info", null, AppResources.AccountLoggedOutSuccessfully); + } + } + + public static async Task OnAccountSwitchAsync() + { + var environmentService = ServiceContainer.Resolve("environmentService"); + await environmentService.SetUrlsFromStorageAsync(); + await ClearServiceCacheAsync(); + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + await deviceActionService.OnAccountSwitchCompleteAsync(); + } + + public static async Task ClearServiceCacheAsync() { - var userService = ServiceContainer.Resolve("userService"); - var syncService = ServiceContainer.Resolve("syncService"); var tokenService = ServiceContainer.Resolve("tokenService"); var cryptoService = ServiceContainer.Resolve("cryptoService"); var settingsService = ServiceContainer.Resolve("settingsService"); var cipherService = ServiceContainer.Resolve("cipherService"); var folderService = ServiceContainer.Resolve("folderService"); var collectionService = ServiceContainer.Resolve("collectionService"); + var sendService = ServiceContainer.Resolve("sendService"); var passwordGenerationService = ServiceContainer.Resolve( "passwordGenerationService"); - var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); - var stateService = ServiceContainer.Resolve("stateService"); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + var policyService = ServiceContainer.Resolve("policyService"); var searchService = ServiceContainer.Resolve("searchService"); - var userId = await userService.GetUserIdAsync(); await Task.WhenAll( - syncService.SetLastSyncAsync(DateTime.MinValue), - tokenService.ClearTokenAsync(), - cryptoService.ClearKeysAsync(), - userService.ClearAsync(), - settingsService.ClearAsync(userId), - cipherService.ClearAsync(userId), - folderService.ClearAsync(userId), - collectionService.ClearAsync(userId), - passwordGenerationService.ClearAsync(), - vaultTimeoutService.ClearAsync(), - stateService.PurgeAsync(), + cipherService.ClearCacheAsync(), deviceActionService.ClearCacheAsync()); - vaultTimeoutService.BiometricLocked = true; + tokenService.ClearCache(); + cryptoService.ClearCache(); + settingsService.ClearCache(); + folderService.ClearCache(); + collectionService.ClearCache(); + sendService.ClearCache(); + passwordGenerationService.ClearCache(); + policyService.ClearCache(); searchService.ClearIndex(); } } diff --git a/src/App/Utilities/I18nExtension.cs b/src/App/Utilities/I18nExtension.cs index 0cb6ed224..b66040c75 100644 --- a/src/App/Utilities/I18nExtension.cs +++ b/src/App/Utilities/I18nExtension.cs @@ -1,6 +1,6 @@ -using Bit.Core.Abstractions; +using System; +using Bit.Core.Abstractions; using Bit.Core.Utilities; -using System; using Xamarin.Forms; using Xamarin.Forms.Xaml; diff --git a/src/App/Utilities/PasswordFormatter.cs b/src/App/Utilities/PasswordFormatter.cs index 8824985ed..a6f25e8ea 100644 --- a/src/App/Utilities/PasswordFormatter.cs +++ b/src/App/Utilities/PasswordFormatter.cs @@ -42,7 +42,7 @@ namespace Bit.App.Utilities { result += "
"; } - + // Start with an otherwise uncovered case so we will definitely enter the "something changed" // state. var currentType = CharType.None; @@ -72,7 +72,7 @@ namespace Bit.App.Utilities { result += ""; } - + currentType = charType; // Switch the color if it is not a normal text. Otherwise leave the @@ -110,13 +110,13 @@ namespace Bit.App.Utilities { result += ""; } - + // Close off iOS div if (Device.RuntimePlatform == Device.iOS) { result += "
"; } - + return result; } } diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs index 9fc24305d..3dabf093d 100644 --- a/src/App/Utilities/ThemeManager.cs +++ b/src/App/Utilities/ThemeManager.cs @@ -1,14 +1,12 @@ using System; -using Bit.App.Models; -using Bit.App.Services; -using Bit.App.Styles; -using Bit.Core; -using Xamarin.Forms; using System.Linq; using System.Threading.Tasks; -#if !FDROID -using Microsoft.AppCenter.Crashes; -#endif +using Bit.App.Models; +using Bit.App.Styles; +using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Xamarin.Forms; namespace Bit.App.Utilities { @@ -76,9 +74,7 @@ namespace Bit.App.Utilities } catch (Exception ex) { -#if !FDROID - Crashes.TrackError(ex); -#endif + LoggerHelper.LogEvenIfCantBeResolved(ex); } } @@ -110,16 +106,15 @@ namespace Bit.App.Utilities } } - public static void SetTheme(bool android, ResourceDictionary resources) + public static void SetTheme(ResourceDictionary resources) { - SetThemeStyle(GetTheme(android), resources); + SetThemeStyle(GetTheme(), resources); } - public static string GetTheme(bool android) + public static string GetTheme() { - return Xamarin.Essentials.Preferences.Get( - string.Format(PreferencesStorageService.KeyFormat, Constants.ThemeKey), default(string), - !android ? "group.com.8bit.bitwarden" : default(string)); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.GetThemeAsync().GetAwaiter().GetResult(); } public static bool OsDarkModeEnabled() @@ -169,9 +164,7 @@ namespace Bit.App.Utilities } catch (Exception ex) { -#if !FDROID - Crashes.TrackError(ex); -#endif + LoggerHelper.LogEvenIfCantBeResolved(ex); } } } diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 782bbe5a2..48405d4e8 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -1,10 +1,10 @@ -using Bit.Core.Models.Domain; -using Bit.Core.Models.Request; -using Bit.Core.Models.Response; -using System; +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; namespace Bit.Core.Abstractions { diff --git a/src/Core/Abstractions/IAuditService.cs b/src/Core/Abstractions/IAuditService.cs index f28bb2e6f..047fb7dad 100644 --- a/src/Core/Abstractions/IAuditService.cs +++ b/src/Core/Abstractions/IAuditService.cs @@ -9,4 +9,4 @@ namespace Bit.Core.Abstractions Task> BreachedAccountsAsync(string username); Task PasswordLeakedAsync(string password); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs index a46cedd9d..b480348a9 100644 --- a/src/Core/Abstractions/IAuthService.cs +++ b/src/Core/Abstractions/IAuthService.cs @@ -24,7 +24,7 @@ namespace Bit.Core.Abstractions Task LogInAsync(string email, string masterPassword, string captchaToken); Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId); Task LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null); - Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null); + Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null); void LogOut(Action callback); void Init(); } diff --git a/src/Core/Abstractions/IBroadcasterService.cs b/src/Core/Abstractions/IBroadcasterService.cs index afdf29f21..ac481b53d 100644 --- a/src/Core/Abstractions/IBroadcasterService.cs +++ b/src/Core/Abstractions/IBroadcasterService.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Domain; -using System; +using System; +using Bit.Core.Models.Domain; namespace Bit.Core.Abstractions { @@ -9,4 +9,4 @@ namespace Bit.Core.Abstractions void Subscribe(string id, Action messageCallback); void Unsubscribe(string id); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs index f5902bf4a..60e4b722c 100644 --- a/src/Core/Abstractions/ICipherService.cs +++ b/src/Core/Abstractions/ICipherService.cs @@ -20,7 +20,7 @@ namespace Bit.Core.Abstractions Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null); Task> GetAllAsync(); Task> GetAllDecryptedAsync(); - Task, List, List>> GetAllDecryptedByUrlAsync(string url, + Task, List, List>> GetAllDecryptedByUrlAsync(string url, List includeOtherTypes = null); Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true); Task> GetAllDecryptedForUrlAsync(string url); diff --git a/src/Core/Abstractions/ICollectionService.cs b/src/Core/Abstractions/ICollectionService.cs index 1f649dba4..21035b421 100644 --- a/src/Core/Abstractions/ICollectionService.cs +++ b/src/Core/Abstractions/ICollectionService.cs @@ -22,4 +22,4 @@ namespace Bit.Core.Abstractions Task UpsertAsync(CollectionData collection); Task UpsertAsync(List collection); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs index 98e58bf48..14f8fdf6c 100644 --- a/src/Core/Abstractions/ICryptoFunctionService.cs +++ b/src/Core/Abstractions/ICryptoFunctionService.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; -using System; +using System; using System.Threading.Tasks; +using Bit.Core.Enums; namespace Bit.Core.Abstractions { diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs index fc9376e63..c06b5b272 100644 --- a/src/Core/Abstractions/ICryptoService.cs +++ b/src/Core/Abstractions/ICryptoService.cs @@ -9,13 +9,14 @@ namespace Bit.Core.Abstractions { public interface ICryptoService { - Task ClearEncKeyAsync(bool memoryOnly = false); - Task ClearKeyAsync(); - Task ClearKeyHashAsync(); - Task ClearKeyPairAsync(bool memoryOnly = false); - Task ClearKeysAsync(); - Task ClearOrgKeysAsync(bool memoryOnly = false); - Task ClearPinProtectedKeyAsync(); + Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null); + Task ClearKeyAsync(string userId = null); + Task ClearKeyHashAsync(string userId = null); + Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null); + Task ClearKeysAsync(string userId = null); + Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null); + Task ClearPinProtectedKeyAsync(string userId = null); + void ClearCache(); Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key); Task DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null); Task DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null); @@ -24,7 +25,7 @@ namespace Bit.Core.Abstractions Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null); Task GetEncKeyAsync(SymmetricCryptoKey key = null); Task> GetFingerprintAsync(string userId, byte[] publicKey = null); - Task GetKeyAsync(); + Task GetKeyAsync(string userId = null); Task GetKeyHashAsync(); Task GetOrgKeyAsync(string orgId); Task> GetOrgKeysAsync(); @@ -33,7 +34,7 @@ namespace Bit.Core.Abstractions Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key); Task HasEncKeyAsync(); Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization); - Task HasKeyAsync(); + Task HasKeyAsync(string userId = null); Task> MakeEncKeyAsync(SymmetricCryptoKey key); Task MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations); Task MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations, diff --git a/src/Core/Abstractions/IEnvironmentService.cs b/src/Core/Abstractions/IEnvironmentService.cs index 8ce168128..7469452b9 100644 --- a/src/Core/Abstractions/IEnvironmentService.cs +++ b/src/Core/Abstractions/IEnvironmentService.cs @@ -13,8 +13,9 @@ namespace Bit.Core.Abstractions string WebVaultUrl { get; set; } string EventsUrl { get; set; } - string GetWebVaultUrl(); + string GetWebVaultUrl(bool returnNullIfDefault = false); + string GetWebSendUrl(); Task SetUrlsAsync(EnvironmentUrlData urls); Task SetUrlsFromStorageAsync(); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IEventService.cs b/src/Core/Abstractions/IEventService.cs index 0f0a51bf0..6a8f8f42e 100644 --- a/src/Core/Abstractions/IEventService.cs +++ b/src/Core/Abstractions/IEventService.cs @@ -9,4 +9,4 @@ namespace Bit.Core.Abstractions Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false); Task UploadEventsAsync(); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IExportService.cs b/src/Core/Abstractions/IExportService.cs index 022500c12..e43f98644 100644 --- a/src/Core/Abstractions/IExportService.cs +++ b/src/Core/Abstractions/IExportService.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Bit.Core.Abstractions { diff --git a/src/Core/Abstractions/IFileUploadService.cs b/src/Core/Abstractions/IFileUploadService.cs index 689b3440f..75f566550 100644 --- a/src/Core/Abstractions/IFileUploadService.cs +++ b/src/Core/Abstractions/IFileUploadService.cs @@ -1,9 +1,11 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Bit.Core.Models.Domain; using Bit.Core.Models.Response; -namespace Bit.Core.Abstractions { - public interface IFileUploadService { +namespace Bit.Core.Abstractions +{ + public interface IFileUploadService + { Task UploadCipherAttachmentFileAsync(AttachmentUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData); Task UploadSendFileAsync(SendFileUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData); } diff --git a/src/Core/Abstractions/IFolderService.cs b/src/Core/Abstractions/IFolderService.cs index 818921fa6..b1feae03d 100644 --- a/src/Core/Abstractions/IFolderService.cs +++ b/src/Core/Abstractions/IFolderService.cs @@ -15,7 +15,7 @@ namespace Bit.Core.Abstractions Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null); Task> GetAllAsync(); Task> GetAllDecryptedAsync(); - Task>> GetAllNestedAsync(); + Task>> GetAllNestedAsync(List folders = null); Task GetAsync(string id); Task> GetNestedAsync(string id); Task ReplaceAsync(Dictionary folders); @@ -23,4 +23,4 @@ namespace Bit.Core.Abstractions Task UpsertAsync(FolderData folder); Task UpsertAsync(List folder); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ILogger.cs b/src/Core/Abstractions/ILogger.cs new file mode 100644 index 000000000..fbb0d50a2 --- /dev/null +++ b/src/Core/Abstractions/ILogger.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ILogger + { + /// + /// Place necessary code to initiaze logger + /// + /// + Task InitAsync(); + + /// + /// Returns if the current logger is enable or disable. + /// + /// + Task IsEnabled(); + + /// + /// Changes the state of the current logger. Setting state enabled to false will block logging. + /// + Task SetEnabled(bool value); + + /// + /// Logs something that is not in itself an exception, e.g. a wrong flow or value that needs to be reported + /// and looked into. + /// + /// A text to be used as the issue's title + /// Additional data + void Error(string message, + IDictionary extraData = null, + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0); + + /// + /// Logs an exception + /// + void Exception(Exception ex); + } +} diff --git a/src/Core/Abstractions/IMessagingService.cs b/src/Core/Abstractions/IMessagingService.cs index 08d631907..5fb5f987d 100644 --- a/src/Core/Abstractions/IMessagingService.cs +++ b/src/Core/Abstractions/IMessagingService.cs @@ -4,4 +4,4 @@ { void Send(string subscriber, object arg = null); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ILogService.cs b/src/Core/Abstractions/INativeLogService.cs similarity index 82% rename from src/Core/Abstractions/ILogService.cs rename to src/Core/Abstractions/INativeLogService.cs index 8af1a806a..c24a622ad 100644 --- a/src/Core/Abstractions/ILogService.cs +++ b/src/Core/Abstractions/INativeLogService.cs @@ -1,10 +1,10 @@ namespace Bit.Core.Abstractions { - public interface ILogService + public interface INativeLogService { void Debug(string message); void Error(string message); void Info(string message); void Warning(string message); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IOrganizationService.cs b/src/Core/Abstractions/IOrganizationService.cs new file mode 100644 index 000000000..07cab5ac0 --- /dev/null +++ b/src/Core/Abstractions/IOrganizationService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IOrganizationService + { + Task GetAsync(string id); + Task GetByIdentifierAsync(string identifier); + Task> GetAllAsync(string userId = null); + Task ReplaceAsync(Dictionary organizations); + Task ClearAllAsync(string userId); + } +} diff --git a/src/Core/Abstractions/IPasswordGenerationService.cs b/src/Core/Abstractions/IPasswordGenerationService.cs index 0bdc80b05..4e52f60a3 100644 --- a/src/Core/Abstractions/IPasswordGenerationService.cs +++ b/src/Core/Abstractions/IPasswordGenerationService.cs @@ -8,7 +8,8 @@ namespace Bit.Core.Abstractions public interface IPasswordGenerationService { Task AddHistoryAsync(string password, CancellationToken token = default(CancellationToken)); - Task ClearAsync(); + Task ClearAsync(string userId = null); + void ClearCache(); Task GeneratePassphraseAsync(PasswordGenerationOptions options); Task GeneratePasswordAsync(PasswordGenerationOptions options); Task> GetHistoryAsync(); diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index 78d9cb224..caabeca3d 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Enums; @@ -10,15 +10,16 @@ namespace Bit.Core.Abstractions public interface IPolicyService { void ClearCache(); - Task> GetAll(PolicyType? type); - Task Replace(Dictionary policies); - Task Clear(string userId); - Task GetMasterPasswordPolicyOptions(IEnumerable policies = null); + Task> GetAll(PolicyType? type, string userId = null); + Task Replace(Dictionary policies, string userId = null); + Task ClearAsync(string userId); + Task GetMasterPasswordPolicyOptions(IEnumerable policies = null, string userId = null); Task EvaluateMasterPassword(int passwordStrength, string newPassword, MasterPasswordPolicyOptions enforcedPolicyOptions); Tuple GetResetPasswordPolicyOptions(IEnumerable policies, string orgId); - Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null); + Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, string userId = null); int? GetPolicyInt(Policy policy, string key); + Task ShouldShowVaultFilterAsync(); } } diff --git a/src/Core/Abstractions/ISendService.cs b/src/Core/Abstractions/ISendService.cs index 91aa2ccae..c6015fa24 100644 --- a/src/Core/Abstractions/ISendService.cs +++ b/src/Core/Abstractions/ISendService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; diff --git a/src/Core/Abstractions/ISettingsService.cs b/src/Core/Abstractions/ISettingsService.cs index a5b780e8d..8ad100f38 100644 --- a/src/Core/Abstractions/ISettingsService.cs +++ b/src/Core/Abstractions/ISettingsService.cs @@ -10,4 +10,4 @@ namespace Bit.Core.Abstractions Task>> GetEquivalentDomainsAsync(); Task SetEquivalentDomainsAsync(List> equivalentDomains); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IStateMigrationService.cs b/src/Core/Abstractions/IStateMigrationService.cs new file mode 100644 index 000000000..bdd9164e3 --- /dev/null +++ b/src/Core/Abstractions/IStateMigrationService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStateMigrationService + { + Task NeedsMigration(); + Task Migrate(); + } +} diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 484ed6d52..8eac4804f 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -1,12 +1,149 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; namespace Bit.Core.Abstractions { public interface IStateService { - Task GetAsync(string key); - Task RemoveAsync(string key); - Task SaveAsync(string key, T obj); - Task PurgeAsync(); + List AccountViews { get; } + Task GetActiveUserIdAsync(); + Task IsActiveAccountAsync(string userId = null); + Task SetActiveUserAsync(string userId); + Task IsAuthenticatedAsync(string userId = null); + Task GetUserIdAsync(string email); + Task RefreshAccountViewsAsync(bool allowAddAccountRow); + Task AddAccountAsync(Account account); + Task LogoutAccountAsync(string userId, bool userInitiated); + Task GetPreAuthEnvironmentUrlsAsync(); + Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value); + Task GetEnvironmentUrlsAsync(string userId = null); + Task GetBiometricUnlockAsync(string userId = null); + Task SetBiometricUnlockAsync(bool? value, string userId = null); + Task GetBiometricLockedAsync(string userId = null); + Task SetBiometricLockedAsync(bool value, string userId = null); + Task CanAccessPremiumAsync(string userId = null); + Task GetProtectedPinAsync(string userId = null); + Task SetProtectedPinAsync(string value, string userId = null); + Task GetPinProtectedAsync(string userId = null); + Task SetPinProtectedAsync(string value, string userId = null); + Task GetPinProtectedKeyAsync(string userId = null); + Task SetPinProtectedKeyAsync(EncString value, string userId = null); + Task GetKdfTypeAsync(string userId = null); + Task SetKdfTypeAsync(KdfType? value, string userId = null); + Task GetKdfIterationsAsync(string userId = null); + Task SetKdfIterationsAsync(int? value, string userId = null); + Task GetKeyEncryptedAsync(string userId = null); + Task SetKeyEncryptedAsync(string value, string userId = null); + Task GetKeyDecryptedAsync(string userId = null); + Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null); + Task GetKeyHashAsync(string userId = null); + Task SetKeyHashAsync(string value, string userId = null); + Task GetEncKeyEncryptedAsync(string userId = null); + Task SetEncKeyEncryptedAsync(string value, string userId = null); + Task> GetOrgKeysEncryptedAsync(string userId = null); + Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null); + Task GetPrivateKeyEncryptedAsync(string userId = null); + Task SetPrivateKeyEncryptedAsync(string value, string userId = null); + Task> GetAutofillBlacklistedUrisAsync(string userId = null); + Task SetAutofillBlacklistedUrisAsync(List value, string userId = null); + Task GetAutofillTileAddedAsync(); + Task SetAutofillTileAddedAsync(bool? value); + Task GetEmailAsync(string userId = null); + Task GetNameAsync(string userId = null); + Task SetNameAsync(string value, string userId = null); + Task GetOrgIdentifierAsync(string userId = null); + Task GetLastActiveTimeAsync(string userId = null); + Task SetLastActiveTimeAsync(long? value, string userId = null); + Task GetVaultTimeoutAsync(string userId = null); + Task SetVaultTimeoutAsync(int? value, string userId = null); + Task GetVaultTimeoutActionAsync(string userId = null); + Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null); + Task GetLastFileCacheClearAsync(); + Task SetLastFileCacheClearAsync(DateTime? value); + Task GetPreviousPageInfoAsync(string userId = null); + Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null); + Task GetInvalidUnlockAttemptsAsync(string userId = null); + Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null); + Task GetLastBuildAsync(); + Task SetLastBuildAsync(string value); + Task GetDisableFaviconAsync(string userId = null); + Task SetDisableFaviconAsync(bool? value, string userId = null); + Task GetDisableAutoTotpCopyAsync(string userId = null); + Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null); + Task GetInlineAutofillEnabledAsync(string userId = null); + Task SetInlineAutofillEnabledAsync(bool? value, string userId = null); + Task GetAutofillDisableSavePromptAsync(string userId = null); + Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null); + Task>> GetLocalDataAsync(string userId = null); + Task SetLocalDataAsync(Dictionary> value, string userId = null); + Task> GetEncryptedCiphersAsync(string userId = null); + Task SetEncryptedCiphersAsync(Dictionary value, string userId = null); + Task GetDefaultUriMatchAsync(string userId = null); + Task SetDefaultUriMatchAsync(int? value, string userId = null); + Task> GetNeverDomainsAsync(string userId = null); + Task SetNeverDomainsAsync(HashSet value, string userId = null); + Task GetClearClipboardAsync(string userId = null); + Task SetClearClipboardAsync(int? value, string userId = null); + Task> GetEncryptedCollectionsAsync(string userId = null); + Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null); + Task GetPasswordRepromptAutofillAsync(string userId = null); + Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null); + Task GetPasswordVerifiedAutofillAsync(string userId = null); + Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null); + Task GetLastSyncAsync(string userId = null); + Task SetLastSyncAsync(DateTime? value, string userId = null); + Task GetSecurityStampAsync(string userId = null); + Task SetSecurityStampAsync(string value, string userId = null); + Task GetEmailVerifiedAsync(string userId = null); + Task SetEmailVerifiedAsync(bool? value, string userId = null); + Task GetSyncOnRefreshAsync(string userId = null); + Task SetSyncOnRefreshAsync(bool? value, string userId = null); + Task GetRememberedEmailAsync(); + Task SetRememberedEmailAsync(string value); + Task GetRememberedOrgIdentifierAsync(); + Task SetRememberedOrgIdentifierAsync(string value); + Task GetThemeAsync(string userId = null); + Task SetThemeAsync(string value, string userId = null); + Task GetAddSitePromptShownAsync(string userId = null); + Task SetAddSitePromptShownAsync(bool? value, string userId = null); + Task GetPushInitialPromptShownAsync(); + Task SetPushInitialPromptShownAsync(bool? value); + Task GetPushLastRegistrationDateAsync(); + Task SetPushLastRegistrationDateAsync(DateTime? value); + Task GetPushInstallationRegistrationErrorAsync(); + Task SetPushInstallationRegistrationErrorAsync(string value); + Task GetPushCurrentTokenAsync(); + Task SetPushCurrentTokenAsync(string value); + Task> GetEventCollectionAsync(); + Task SetEventCollectionAsync(List value); + Task> GetEncryptedFoldersAsync(string userId = null); + Task SetEncryptedFoldersAsync(Dictionary value, string userId = null); + Task> GetEncryptedPoliciesAsync(string userId = null); + Task SetEncryptedPoliciesAsync(Dictionary value, string userId = null); + Task GetPushRegisteredTokenAsync(); + Task SetPushRegisteredTokenAsync(string value); + Task GetUsesKeyConnectorAsync(string userId = null); + Task SetUsesKeyConnectorAsync(bool? value, string userId = null); + Task> GetOrganizationsAsync(string userId = null); + Task SetOrganizationsAsync(Dictionary organizations, string userId = null); + Task GetPasswordGenerationOptionsAsync(string userId = null); + Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null); + Task> GetEncryptedPasswordGenerationHistory(string userId = null); + Task SetEncryptedPasswordGenerationHistoryAsync(List value, string userId = null); + Task> GetEncryptedSendsAsync(string userId = null); + Task SetEncryptedSendsAsync(Dictionary value, string userId = null); + Task> GetSettingsAsync(string userId = null); + Task SetSettingsAsync(Dictionary value, string userId = null); + Task GetAccessTokenAsync(string userId = null); + Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetRefreshTokenAsync(string userId = null); + Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetTwoFactorTokenAsync(string email = null); + Task SetTwoFactorTokenAsync(string value, string email = null); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ITokenService.cs b/src/Core/Abstractions/ITokenService.cs index b08391865..578c97d8f 100644 --- a/src/Core/Abstractions/ITokenService.cs +++ b/src/Core/Abstractions/ITokenService.cs @@ -6,15 +6,16 @@ namespace Bit.Core.Abstractions { public interface ITokenService { - Task ClearTokenAsync(); + Task ClearTokenAsync(string userId = null); Task ClearTwoFactorTokenAsync(string email); + void ClearCache(); JObject DecodeToken(); string GetEmail(); bool GetEmailVerified(); string GetIssuer(); string GetName(); bool GetPremium(); - bool GetIsExternal(); + Task GetIsExternal(); Task GetRefreshTokenAsync(); Task GetTokenAsync(); Task ToggleTokensAsync(); @@ -22,7 +23,7 @@ namespace Bit.Core.Abstractions Task GetTwoFactorTokenAsync(string email); string GetUserId(); Task SetRefreshTokenAsync(string refreshToken); - Task SetTokenAsync(string token); + Task SetAccessTokenAsync(string token, bool forDecodeOnly = false); Task SetTokensAsync(string accessToken, string refreshToken); Task SetTwoFactorTokenAsync(string token, string email); bool TokenNeedsRefresh(int minutes = 5); diff --git a/src/Core/Abstractions/ITotpService.cs b/src/Core/Abstractions/ITotpService.cs index 0de407e3e..e0aff3c2f 100644 --- a/src/Core/Abstractions/ITotpService.cs +++ b/src/Core/Abstractions/ITotpService.cs @@ -8,4 +8,4 @@ namespace Bit.Core.Abstractions int GetTimeInterval(string key); Task IsAutoCopyEnabledAsync(); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IUserService.cs b/src/Core/Abstractions/IUserService.cs deleted file mode 100644 index d7aaef82b..000000000 --- a/src/Core/Abstractions/IUserService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; - -namespace Bit.Core.Abstractions -{ - public interface IUserService - { - Task CanAccessPremiumAsync(); - Task ClearAsync(); - Task ClearOrganizationsAsync(string userId); - Task> GetAllOrganizationAsync(); - Task GetEmailAsync(); - Task GetKdfAsync(); - Task GetKdfIterationsAsync(); - Task GetOrganizationAsync(string id); - Task GetOrganizationByIdentifierAsync(string identifier); - Task GetSecurityStampAsync(); - Task GetEmailVerifiedAsync(); - Task GetForcePasswordReset(); - Task GetUserIdAsync(); - Task IsAuthenticatedAsync(); - Task ReplaceOrganizationsAsync(Dictionary organizations); - Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations); - Task SetSecurityStampAsync(string stamp); - Task SetEmailVerifiedAsync(bool emailVerified); - Task SetForcePasswordReset(bool forcePasswordReset); - } -} diff --git a/src/Core/Abstractions/IVaultTimeoutService.cs b/src/Core/Abstractions/IVaultTimeoutService.cs index bcdb776a8..ed71b7f41 100644 --- a/src/Core/Abstractions/IVaultTimeoutService.cs +++ b/src/Core/Abstractions/IVaultTimeoutService.cs @@ -1,23 +1,26 @@ using System; using System.Threading.Tasks; -using Bit.Core.Models.Domain; +using Bit.Core.Enums; namespace Bit.Core.Abstractions { public interface IVaultTimeoutService { - EncString PinProtectedKey { get; set; } - bool BiometricLocked { get; set; } long? DelayLockAndLogoutMs { get; set; } Task CheckVaultTimeoutAsync(); - Task ClearAsync(); - Task IsLockedAsync(); - Task> IsPinLockSetAsync(); - Task IsBiometricLockSetAsync(); - Task LockAsync(bool allowSoftLock = false, bool userInitiated = false); - Task LogOutAsync(); - Task SetVaultTimeoutOptionsAsync(int? timeout, string action); - Task GetVaultTimeout(); + Task ShouldTimeoutAsync(string userId = null); + Task ExecuteTimeoutActionAsync(string userId = null); + Task ClearAsync(string userId = null); + Task IsLockedAsync(string userId = null); + Task ShouldLockAsync(string userId = null); + Task IsLoggedOutByTimeoutAsync(string userId = null); + Task ShouldLogOutByTimeoutAsync(string userId = null); + Task> IsPinLockSetAsync(string userId = null); + Task IsBiometricLockSetAsync(string userId = null); + Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null); + Task LogOutAsync(bool userInitiated = true, string userId = null); + Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action); + Task GetVaultTimeout(string userId = null); } } diff --git a/src/Core/BitwardenIcons.cs b/src/Core/BitwardenIcons.cs index c659d9bea..7e972baba 100644 --- a/src/Core/BitwardenIcons.cs +++ b/src/Core/BitwardenIcons.cs @@ -1,4 +1,4 @@ -namespace Bit.Core +namespace Bit.Core { public static class BitwardenIcons { @@ -111,5 +111,6 @@ namespace Bit.Core public const string EyeSlash = "\xe96d"; public const string File = "\xe96e"; public const string Paste = "\xe96f"; + public const string ViewCellMenu = "\xe5d3"; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1751856aa..996358876 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -2,32 +2,20 @@ { public static class Constants { + public const int MaxAccounts = 5; public const string AndroidAppProtocol = "androidapp://"; public const string iOSAppProtocol = "iosapp://"; - public static string SyncOnRefreshKey = "syncOnRefresh"; - public static string VaultTimeoutKey = "lockOption"; - public static string VaultTimeoutActionKey = "vaultTimeoutAction"; - public static string LastActiveTimeKey = "lastActiveTime"; - public static string BiometricUnlockKey = "fingerprintUnlock"; - public static string ProtectedPin = "protectedPin"; - public static string PinProtectedKey = "pinProtectedKey"; - public static string DefaultUriMatch = "defaultUriMatch"; - public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; - public static string EnvironmentUrlsKey = "environmentUrls"; + public static string StateVersionKey = "stateVersion"; + public static string StateKey = "state"; + public static string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; public static string LastFileCacheClearKey = "lastFileCacheClear"; - public static string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; - public static string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; public static string AutofillTileAdded = "autofillTileAdded"; - public static string DisableFaviconKey = "disableFavicon"; public static string PushRegisteredTokenKey = "pushRegisteredToken"; public static string PushCurrentTokenKey = "pushCurrentToken"; public static string PushLastRegistrationDateKey = "pushLastRegistrationDate"; public static string PushInitialPromptShownKey = "pushInitialPromptShown"; - public static string PushInstallationRegistrationError = "pushInstallationRegistrationError"; - public static string ThemeKey = "theme"; - public static string ClearClipboardKey = "clearClipboard"; + public static string PushInstallationRegistrationErrorKey = "pushInstallationRegistrationError"; public static string LastBuildKey = "lastBuild"; - public static string OldUserIdKey = "userId"; public static string AddSitePromptShownKey = "addSitePromptShown"; public static string ClearCiphersCacheKey = "clearCiphersCache"; public static string BiometricIntegrityKey = "biometricIntegrityState"; @@ -35,29 +23,63 @@ public static string iOSAutoFillBiometricIntegrityKey = "iOSAutoFillBiometricIntegrityState"; public static string iOSExtensionClearCiphersCacheKey = "iOSExtensionClearCiphersCache"; public static string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState"; - public static string MigratedFromV1 = "migratedFromV1"; - public static string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; - public static string TriedV1Resync = "triedV1Resync"; + public static string iOSShareExtensionClearCiphersCacheKey = "iOSShareExtensionClearCiphersCache"; + public static string iOSShareExtensionBiometricIntegrityKey = "iOSShareExtensionBiometricIntegrityState"; public static string EventCollectionKey = "eventCollection"; - public static string PreviousPageKey = "previousPage"; - public static string InlineAutofillEnabledKey = "inlineAutofillEnabled"; - public static string InvalidUnlockAttempts = "invalidUnlockAttempts"; - public static string PasswordRepromptAutofillKey = "passwordRepromptAutofillKey"; - public static string PasswordVerifiedAutofillKey = "passwordVerifiedAutofillKey"; + public static string RememberedEmailKey = "rememberedEmail"; + public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier"; public const int SelectFileRequestCode = 42; public const int SelectFilePermissionRequestCode = 43; public const int SaveFileRequestCode = 44; - + public static readonly string[] AndroidAllClearCipherCacheKeys = { ClearCiphersCacheKey }; - + public static readonly string[] iOSAllClearCipherCacheKeys = { ClearCiphersCacheKey, iOSAutoFillClearCiphersCacheKey, - iOSExtensionClearCiphersCacheKey + iOSExtensionClearCiphersCacheKey, + iOSShareExtensionClearCiphersCacheKey }; + + public static string CiphersKey(string userId) => $"ciphers_{userId}"; + public static string FoldersKey(string userId) => $"folders_{userId}"; + public static string CollectionsKey(string userId) => $"collections_{userId}"; + public static string OrganizationsKey(string userId) => $"organizations_{userId}"; + public static string LocalDataKey(string userId) => $"ciphersLocalData_{userId}"; + public static string NeverDomainsKey(string userId) => $"neverDomains_{userId}"; + public static string SendsKey(string userId) => $"sends_{userId}"; + public static string PoliciesKey(string userId) => $"policies_{userId}"; + public static string KeyKey(string userId) => $"key_{userId}"; + public static string EncOrgKeysKey(string userId) => $"encOrgKeys_{userId}"; + public static string EncPrivateKeyKey(string userId) => $"encPrivateKey_{userId}"; + public static string EncKeyKey(string userId) => $"encKey_{userId}"; + public static string KeyHashKey(string userId) => $"keyHash_{userId}"; + public static string PinProtectedKey(string userId) => $"pinProtectedKey_{userId}"; + public static string PassGenOptionsKey(string userId) => $"passwordGenerationOptions_{userId}"; + public static string PassGenHistoryKey(string userId) => $"generatedPasswordHistory_{userId}"; + public static string TwoFactorTokenKey(string email) => $"twoFactorToken_{email}"; + public static string LastActiveTimeKey(string userId) => $"lastActiveTime_{userId}"; + public static string InvalidUnlockAttemptsKey(string userId) => $"invalidUnlockAttempts_{userId}"; + public static string InlineAutofillEnabledKey(string userId) => $"inlineAutofillEnabled_{userId}"; + public static string AutofillDisableSavePromptKey(string userId) => $"autofillDisableSavePrompt_{userId}"; + public static string AutofillBlacklistedUrisKey(string userId) => $"autofillBlacklistedUris_{userId}"; + public static string ClearClipboardKey(string userId) => $"clearClipboard_{userId}"; + public static string SyncOnRefreshKey(string userId) => $"syncOnRefresh_{userId}"; + public static string DisableFaviconKey(string userId) => $"disableFavicon_{userId}"; + public static string DefaultUriMatchKey(string userId) => $"defaultUriMatch_{userId}"; + public static string ThemeKey(string userId) => $"theme_{userId}"; + public static string DisableAutoTotpCopyKey(string userId) => $"disableAutoTotpCopy_{userId}"; + public static string PreviousPageKey(string userId) => $"previousPage_{userId}"; + public static string PasswordRepromptAutofillKey(string userId) => $"passwordRepromptAutofillKey_{userId}"; + public static string PasswordVerifiedAutofillKey(string userId) => $"passwordVerifiedAutofillKey_{userId}"; + public static string SettingsKey(string userId) => $"settings_{userId}"; + public static string UsesKeyConnectorKey(string userId) => $"usesKeyConnector_{userId}"; + public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; + public static string LastSyncKey(string userId) => $"lastSync_{userId}"; + public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 09129dec5..6aceb3c57 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -15,6 +15,8 @@ + + @@ -31,4 +33,7 @@ + + + diff --git a/src/Core/Enums/AuthenticationStatus.cs b/src/Core/Enums/AuthenticationStatus.cs new file mode 100644 index 000000000..2d81e071d --- /dev/null +++ b/src/Core/Enums/AuthenticationStatus.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum AuthenticationStatus : byte + { + LoggedOut = 0, + Locked = 1, + Unlocked = 2, + } +} diff --git a/src/Core/Enums/ClientType.cs b/src/Core/Enums/ClientType.cs index f6d3fd9c2..9d5e4feab 100644 --- a/src/Core/Enums/ClientType.cs +++ b/src/Core/Enums/ClientType.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Enums { - public enum ClientType: byte + public enum ClientType : byte { Web = 1, Browser = 2, diff --git a/src/Core/Enums/HdkfAlgorithm.cs b/src/Core/Enums/HdkfAlgorithm.cs index 5837a3fa3..0f2b32e17 100644 --- a/src/Core/Enums/HdkfAlgorithm.cs +++ b/src/Core/Enums/HdkfAlgorithm.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Enums +namespace Bit.Core.Enums { public enum HkdfAlgorithm : byte { diff --git a/src/Core/Enums/LinkedIdType.cs b/src/Core/Enums/LinkedIdType.cs index 32803868c..2ffd0acb5 100644 --- a/src/Core/Enums/LinkedIdType.cs +++ b/src/Core/Enums/LinkedIdType.cs @@ -1,6 +1,7 @@ -namespace Bit.Core.Enums { - - public enum LinkedIdType: int +namespace Bit.Core.Enums +{ + + public enum LinkedIdType : int { // Login Login_Username = 100, diff --git a/src/Core/Enums/SendType.cs b/src/Core/Enums/SendType.cs index f8f06f13d..fb447b2b5 100644 --- a/src/Core/Enums/SendType.cs +++ b/src/Core/Enums/SendType.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Enums +namespace Bit.Core.Enums { public enum SendType { diff --git a/src/Core/Enums/StorageLocation.cs b/src/Core/Enums/StorageLocation.cs new file mode 100644 index 000000000..faa9476c4 --- /dev/null +++ b/src/Core/Enums/StorageLocation.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum StorageLocation + { + Both = 0, + Disk = 1, + Memory = 2 + } +} diff --git a/src/Core/Enums/VaultTimeoutAction.cs b/src/Core/Enums/VaultTimeoutAction.cs new file mode 100644 index 000000000..5a9c33909 --- /dev/null +++ b/src/Core/Enums/VaultTimeoutAction.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum VaultTimeoutAction + { + Lock = 0, + Logout = 1, + } +} diff --git a/src/Core/Exceptions/ApiException.cs b/src/Core/Exceptions/ApiException.cs index bf0aa9791..493900137 100644 --- a/src/Core/Exceptions/ApiException.cs +++ b/src/Core/Exceptions/ApiException.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Response; -using System; +using System; +using Bit.Core.Models.Response; namespace Bit.Core.Exceptions { diff --git a/src/Core/Models/Api/SendFileApi.cs b/src/Core/Models/Api/SendFileApi.cs index ae7f01aea..345d9f215 100644 --- a/src/Core/Models/Api/SendFileApi.cs +++ b/src/Core/Models/Api/SendFileApi.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Api +namespace Bit.Core.Models.Api { public class SendFileApi { diff --git a/src/Core/Models/Api/SendTextApi.cs b/src/Core/Models/Api/SendTextApi.cs index 9520031da..2b7b92429 100644 --- a/src/Core/Models/Api/SendTextApi.cs +++ b/src/Core/Models/Api/SendTextApi.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Api +namespace Bit.Core.Models.Api { public class SendTextApi { diff --git a/src/Core/Models/Data/CipherData.cs b/src/Core/Models/Data/CipherData.cs index 50cb20133..c571771a7 100644 --- a/src/Core/Models/Data/CipherData.cs +++ b/src/Core/Models/Data/CipherData.cs @@ -1,8 +1,8 @@ -using Bit.Core.Models.Response; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using Bit.Core.Models.Response; namespace Bit.Core.Models.Data { diff --git a/src/Core/Models/Data/EventData.cs b/src/Core/Models/Data/EventData.cs index 49f0957f3..6a5007033 100644 --- a/src/Core/Models/Data/EventData.cs +++ b/src/Core/Models/Data/EventData.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using System; +using System; +using Bit.Core.Enums; namespace Bit.Core.Models.Data { diff --git a/src/Core/Models/Data/FolderData.cs b/src/Core/Models/Data/FolderData.cs index 24a155a05..75e56d16e 100644 --- a/src/Core/Models/Data/FolderData.cs +++ b/src/Core/Models/Data/FolderData.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Response; -using System; +using System; +using Bit.Core.Models.Response; namespace Bit.Core.Models.Data { diff --git a/src/Core/Models/Data/LoginData.cs b/src/Core/Models/Data/LoginData.cs index 7afff7e36..286c542ed 100644 --- a/src/Core/Models/Data/LoginData.cs +++ b/src/Core/Models/Data/LoginData.cs @@ -1,7 +1,7 @@ -using Bit.Core.Models.Api; -using System; +using System; using System.Collections.Generic; using System.Linq; +using Bit.Core.Models.Api; namespace Bit.Core.Models.Data { diff --git a/src/Core/Models/Data/PasswordHistoryData.cs b/src/Core/Models/Data/PasswordHistoryData.cs index 5d15bc0a7..9ade26ef2 100644 --- a/src/Core/Models/Data/PasswordHistoryData.cs +++ b/src/Core/Models/Data/PasswordHistoryData.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Response; -using System; +using System; +using Bit.Core.Models.Response; namespace Bit.Core.Models.Data { diff --git a/src/Core/Models/Data/Permissions.cs b/src/Core/Models/Data/Permissions.cs index 4c6e7fe3a..c1b8278f2 100644 --- a/src/Core/Models/Data/Permissions.cs +++ b/src/Core/Models/Data/Permissions.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data { public class Permissions { diff --git a/src/App/Models/PreviousPageInfo.cs b/src/Core/Models/Data/PreviousPageInfo.cs similarity index 86% rename from src/App/Models/PreviousPageInfo.cs rename to src/Core/Models/Data/PreviousPageInfo.cs index 3f6bba7de..bcf8e29c3 100644 --- a/src/App/Models/PreviousPageInfo.cs +++ b/src/Core/Models/Data/PreviousPageInfo.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models +namespace Bit.Core.Models.Data { public class PreviousPageInfo { diff --git a/src/Core/Models/Data/SendData.cs b/src/Core/Models/Data/SendData.cs index ce891b3cc..a4659c37f 100644 --- a/src/Core/Models/Data/SendData.cs +++ b/src/Core/Models/Data/SendData.cs @@ -1,4 +1,4 @@ -using System; +using System; using Bit.Core.Enums; using Bit.Core.Models.Response; @@ -38,7 +38,7 @@ namespace Bit.Core.Models.Data break; } } - + public string Id { get; set; } public string AccessId { get; set; } public string UserId { get; set; } diff --git a/src/Core/Models/Data/SendFileData.cs b/src/Core/Models/Data/SendFileData.cs index 65027d6a5..d596bde64 100644 --- a/src/Core/Models/Data/SendFileData.cs +++ b/src/Core/Models/Data/SendFileData.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using Bit.Core.Models.Api; namespace Bit.Core.Models.Data diff --git a/src/Core/Models/Data/SendTextData.cs b/src/Core/Models/Data/SendTextData.cs index 2e83ca1a0..ae4f4abfc 100644 --- a/src/Core/Models/Data/SendTextData.cs +++ b/src/Core/Models/Data/SendTextData.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using Bit.Core.Models.Api; namespace Bit.Core.Models.Data diff --git a/src/Core/Models/Domain/Account.cs b/src/Core/Models/Domain/Account.cs new file mode 100644 index 000000000..cc65e92fe --- /dev/null +++ b/src/Core/Models/Domain/Account.cs @@ -0,0 +1,111 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Account : Domain + { + public AccountProfile Profile; + public AccountTokens Tokens; + public AccountSettings Settings; + public AccountVolatileData VolatileData; + + public Account() { } + + public Account(AccountProfile profile, AccountTokens tokens) + { + Profile = profile; + Tokens = tokens; + Settings = new AccountSettings(); + VolatileData = new AccountVolatileData(); + } + + public Account(Account account) + { + // Copy constructor excludes VolatileData (for storage) + Profile = new AccountProfile(account.Profile); + Tokens = new AccountTokens(account.Tokens); + Settings = new AccountSettings(account.Settings); + } + + public class AccountProfile + { + public AccountProfile() { } + + public AccountProfile(AccountProfile copy) + { + if (copy == null) + { + return; + } + + UserId = copy.UserId; + Email = copy.Email; + Name = copy.Name; + Stamp = copy.Stamp; + OrgIdentifier = copy.OrgIdentifier; + KdfType = copy.KdfType; + KdfIterations = copy.KdfIterations; + EmailVerified = copy.EmailVerified; + HasPremiumPersonally = copy.HasPremiumPersonally; + } + + public string UserId; + public string Email; + public string Name; + public string Stamp; + public string OrgIdentifier; + public KdfType? KdfType; + public int? KdfIterations; + public bool? EmailVerified; + public bool? HasPremiumPersonally; + } + + public class AccountTokens + { + public AccountTokens() { } + + public AccountTokens(AccountTokens copy) + { + if (copy == null) + { + return; + } + + AccessToken = copy.AccessToken; + RefreshToken = copy.RefreshToken; + } + + public string AccessToken; + public string RefreshToken; + } + + public class AccountSettings + { + public AccountSettings() { } + + public AccountSettings(AccountSettings copy) + { + if (copy == null) + { + return; + } + + EnvironmentUrls = copy.EnvironmentUrls; + VaultTimeout = copy.VaultTimeout; + VaultTimeoutAction = copy.VaultTimeoutAction; + } + + public EnvironmentUrlData EnvironmentUrls; + public int? VaultTimeout; + public VaultTimeoutAction? VaultTimeoutAction; + } + + public class AccountVolatileData + { + public SymmetricCryptoKey Key; + public EncString PinProtectedKey; + public bool? BiometricLocked; + } + } +} diff --git a/src/Core/Models/Domain/Attachment.cs b/src/Core/Models/Domain/Attachment.cs index b46f66a31..3e71d6107 100644 --- a/src/Core/Models/Domain/Attachment.cs +++ b/src/Core/Models/Domain/Attachment.cs @@ -1,9 +1,9 @@ -using Bit.Core.Abstractions; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Bit.Core.Models.Domain { @@ -39,7 +39,7 @@ namespace Bit.Core.Models.Domain { "FileName" }, orgId); - + if (Key != null) { var cryptoService = ServiceContainer.Resolve("cryptoService"); diff --git a/src/Core/Models/Domain/AuthResult.cs b/src/Core/Models/Domain/AuthResult.cs index 404cea660..8e48c7da7 100644 --- a/src/Core/Models/Domain/AuthResult.cs +++ b/src/Core/Models/Domain/AuthResult.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using System.Collections.Generic; +using System.Collections.Generic; +using Bit.Core.Enums; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Card.cs b/src/Core/Models/Domain/Card.cs index b8c96da02..8a7ffff23 100644 --- a/src/Core/Models/Domain/Card.cs +++ b/src/Core/Models/Domain/Card.cs @@ -1,7 +1,7 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs index cf37b6c0b..d744f6a57 100644 --- a/src/Core/Models/Domain/Cipher.cs +++ b/src/Core/Models/Domain/Cipher.cs @@ -1,10 +1,10 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Collection.cs b/src/Core/Models/Domain/Collection.cs index 4fd4d5551..da62c55e2 100644 --- a/src/Core/Models/Domain/Collection.cs +++ b/src/Core/Models/Domain/Collection.cs @@ -1,7 +1,7 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/EncByteArray.cs b/src/Core/Models/Domain/EncByteArray.cs index a3214cad3..97814a7aa 100644 --- a/src/Core/Models/Domain/EncByteArray.cs +++ b/src/Core/Models/Domain/EncByteArray.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Domain +namespace Bit.Core.Models.Domain { public class EncByteArray { diff --git a/src/Core/Models/Domain/EncString.cs b/src/Core/Models/Domain/EncString.cs index df4757b9e..2a86348a6 100644 --- a/src/Core/Models/Domain/EncString.cs +++ b/src/Core/Models/Domain/EncString.cs @@ -1,8 +1,8 @@ -using Bit.Core.Abstractions; +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; -using System; -using System.Threading.Tasks; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Field.cs b/src/Core/Models/Domain/Field.cs index 31f5d4340..cbfdcbca3 100644 --- a/src/Core/Models/Domain/Field.cs +++ b/src/Core/Models/Domain/Field.cs @@ -1,8 +1,8 @@ -using Bit.Core.Enums; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.View; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Folder.cs b/src/Core/Models/Domain/Folder.cs index 8b1b9429e..fe59ef5f0 100644 --- a/src/Core/Models/Domain/Folder.cs +++ b/src/Core/Models/Domain/Folder.cs @@ -1,8 +1,8 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Identity.cs b/src/Core/Models/Domain/Identity.cs index edc5869cb..4a705dcb6 100644 --- a/src/Core/Models/Domain/Identity.cs +++ b/src/Core/Models/Domain/Identity.cs @@ -1,7 +1,7 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Login.cs b/src/Core/Models/Domain/Login.cs index abc9ead94..00c0ab07b 100644 --- a/src/Core/Models/Domain/Login.cs +++ b/src/Core/Models/Domain/Login.cs @@ -1,9 +1,9 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/LoginUri.cs b/src/Core/Models/Domain/LoginUri.cs index 7acba40b5..31747fd96 100644 --- a/src/Core/Models/Domain/LoginUri.cs +++ b/src/Core/Models/Domain/LoginUri.cs @@ -1,8 +1,8 @@ -using Bit.Core.Enums; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.View; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/PasswordGenerationOptions.cs b/src/Core/Models/Domain/PasswordGenerationOptions.cs index 9f0717c2b..16afec6bf 100644 --- a/src/Core/Models/Domain/PasswordGenerationOptions.cs +++ b/src/Core/Models/Domain/PasswordGenerationOptions.cs @@ -9,7 +9,7 @@ if (defaultOptions) { Length = 14; - Ambiguous = false; + AllowAmbiguousChar = true; Number = true; MinNumber = 1; Uppercase = true; @@ -27,7 +27,7 @@ } public int? Length { get; set; } - public bool? Ambiguous { get; set; } + public bool? AllowAmbiguousChar { get; set; } public bool? Number { get; set; } public int? MinNumber { get; set; } public bool? Uppercase { get; set; } @@ -45,7 +45,7 @@ public void Merge(PasswordGenerationOptions defaults) { Length = Length ?? defaults.Length; - Ambiguous = Ambiguous ?? defaults.Ambiguous; + AllowAmbiguousChar = AllowAmbiguousChar ?? defaults.AllowAmbiguousChar; Number = Number ?? defaults.Number; MinNumber = MinNumber ?? defaults.MinNumber; Uppercase = Uppercase ?? defaults.Uppercase; diff --git a/src/Core/Models/Domain/PasswordHistory.cs b/src/Core/Models/Domain/PasswordHistory.cs index b9d1e9ef0..6ae1ab1f1 100644 --- a/src/Core/Models/Domain/PasswordHistory.cs +++ b/src/Core/Models/Domain/PasswordHistory.cs @@ -1,8 +1,8 @@ -using Bit.Core.Models.Data; -using Bit.Core.Models.View; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Policy.cs b/src/Core/Models/Domain/Policy.cs index 470c92855..a84b095d0 100644 --- a/src/Core/Models/Domain/Policy.cs +++ b/src/Core/Models/Domain/Policy.cs @@ -16,7 +16,7 @@ namespace Bit.Core.Models.Domain Data = obj.Data; Enabled = obj.Enabled; } - + public string Id { get; set; } public string OrganizationId { get; set; } public PolicyType Type { get; set; } diff --git a/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs b/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs index 1653aa774..4e9cebe2e 100644 --- a/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs +++ b/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Domain +namespace Bit.Core.Models.Domain { public class ResetPasswordPolicyOptions { diff --git a/src/Core/Models/Domain/SecureNote.cs b/src/Core/Models/Domain/SecureNote.cs index 6d3afab4c..55817ab15 100644 --- a/src/Core/Models/Domain/SecureNote.cs +++ b/src/Core/Models/Domain/SecureNote.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.View; -using System.Threading.Tasks; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Domain/Send.cs b/src/Core/Models/Domain/Send.cs index 2c7761d41..4ecea0c15 100644 --- a/src/Core/Models/Domain/Send.cs +++ b/src/Core/Models/Domain/Send.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Abstractions; diff --git a/src/Core/Models/Domain/SendFile.cs b/src/Core/Models/Domain/SendFile.cs index 77b24f3f6..cde9086d1 100644 --- a/src/Core/Models/Domain/SendFile.cs +++ b/src/Core/Models/Domain/SendFile.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Models.Data; using Bit.Core.Models.View; diff --git a/src/Core/Models/Domain/SendText.cs b/src/Core/Models/Domain/SendText.cs index f7825343e..adff8251f 100644 --- a/src/Core/Models/Domain/SendText.cs +++ b/src/Core/Models/Domain/SendText.cs @@ -1,4 +1,4 @@ - + using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Models.Data; diff --git a/src/Core/Models/Domain/State.cs b/src/Core/Models/Domain/State.cs new file mode 100644 index 000000000..f8180fab8 --- /dev/null +++ b/src/Core/Models/Domain/State.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Domain +{ + public class State : Domain + { + public Dictionary Accounts { get; set; } + public string ActiveUserId { get; set; } + } +} diff --git a/src/Core/Models/Domain/StorageOptions.cs b/src/Core/Models/Domain/StorageOptions.cs new file mode 100644 index 000000000..0d9061876 --- /dev/null +++ b/src/Core/Models/Domain/StorageOptions.cs @@ -0,0 +1,13 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class StorageOptions : Domain + { + public StorageLocation? StorageLocation { get; set; } + public bool? UseSecureStorage { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public bool? SkipTokenStorage { get; set; } + } +} diff --git a/src/Core/Models/Domain/SymmetricCryptoKey.cs b/src/Core/Models/Domain/SymmetricCryptoKey.cs index 820823b26..91bce7550 100644 --- a/src/Core/Models/Domain/SymmetricCryptoKey.cs +++ b/src/Core/Models/Domain/SymmetricCryptoKey.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; -using System; +using System; using System.Linq; +using Bit.Core.Enums; namespace Bit.Core.Models.Domain { diff --git a/src/Core/Models/Export/CollectionWithId.cs b/src/Core/Models/Export/CollectionWithId.cs index 2b8af4dd2..01e80f250 100644 --- a/src/Core/Models/Export/CollectionWithId.cs +++ b/src/Core/Models/Export/CollectionWithId.cs @@ -10,7 +10,7 @@ namespace Bit.Core.Models.Export Id = obj.Id; } - public CollectionWithId(Domain.Collection obj): base(obj) + public CollectionWithId(Domain.Collection obj) : base(obj) { Id = obj.Id; } diff --git a/src/Core/Models/Request/CipherCreateRequest.cs b/src/Core/Models/Request/CipherCreateRequest.cs index aefb9b340..bee9b3c64 100644 --- a/src/Core/Models/Request/CipherCreateRequest.cs +++ b/src/Core/Models/Request/CipherCreateRequest.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.Domain; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.Request { diff --git a/src/Core/Models/Request/CipherRequest.cs b/src/Core/Models/Request/CipherRequest.cs index ca00ee182..82c029751 100644 --- a/src/Core/Models/Request/CipherRequest.cs +++ b/src/Core/Models/Request/CipherRequest.cs @@ -1,9 +1,9 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Api; -using Bit.Core.Models.Domain; +using System; using System.Collections.Generic; using System.Linq; -using System; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.Request { diff --git a/src/Core/Models/Request/CipherShareRequest.cs b/src/Core/Models/Request/CipherShareRequest.cs index 4fb4f3761..89bfb6dc6 100644 --- a/src/Core/Models/Request/CipherShareRequest.cs +++ b/src/Core/Models/Request/CipherShareRequest.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.Domain; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.Request { diff --git a/src/Core/Models/Request/EventRequest.cs b/src/Core/Models/Request/EventRequest.cs index d1c728287..da4124189 100644 --- a/src/Core/Models/Request/EventRequest.cs +++ b/src/Core/Models/Request/EventRequest.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using System; +using System; +using Bit.Core.Enums; namespace Bit.Core.Models.Request { diff --git a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs index b751a9a4f..267a425ec 100644 --- a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs +++ b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Request +namespace Bit.Core.Models.Request { public class OrganizationUserResetPasswordEnrollmentRequest { diff --git a/src/Core/Models/Request/SendRequest.cs b/src/Core/Models/Request/SendRequest.cs index fb3d643a9..abd7323ec 100644 --- a/src/Core/Models/Request/SendRequest.cs +++ b/src/Core/Models/Request/SendRequest.cs @@ -1,4 +1,4 @@ -using System; +using System; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Domain; @@ -23,7 +23,7 @@ namespace Bit.Core.Models.Request public SendRequest(Send send, long? fileLength) { - Type = send.Type ; + Type = send.Type; FileLength = fileLength; Name = send.Name?.EncryptedString; Notes = send.Notes?.EncryptedString; diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs index b982c2ea4..fe299e455 100644 --- a/src/Core/Models/Request/TokenRequest.cs +++ b/src/Core/Models/Request/TokenRequest.cs @@ -1,9 +1,9 @@ -using Bit.Core.Enums; -using Bit.Core.Utilities; -using System; +using System; using System.Collections.Generic; using System.Net.Http.Headers; using System.Text; +using Bit.Core.Enums; +using Bit.Core.Utilities; namespace Bit.Core.Models.Request { diff --git a/src/Core/Models/Request/TwoFactorEmailRequest.cs b/src/Core/Models/Request/TwoFactorEmailRequest.cs index 3446df586..2b5784099 100644 --- a/src/Core/Models/Request/TwoFactorEmailRequest.cs +++ b/src/Core/Models/Request/TwoFactorEmailRequest.cs @@ -4,5 +4,6 @@ { public string Email { get; set; } public string MasterPasswordHash { get; set; } + public string DeviceIdentifier { get; set; } } } diff --git a/src/Core/Models/Response/AttachmentUploadDataReponse.cs b/src/Core/Models/Response/AttachmentUploadDataReponse.cs index 5f1a76c37..e26059d9f 100644 --- a/src/Core/Models/Response/AttachmentUploadDataReponse.cs +++ b/src/Core/Models/Response/AttachmentUploadDataReponse.cs @@ -1,4 +1,4 @@ -using Bit.Core.Enums; +using Bit.Core.Enums; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/CipherResponse.cs b/src/Core/Models/Response/CipherResponse.cs index 684d5f82d..070d41eee 100644 --- a/src/Core/Models/Response/CipherResponse.cs +++ b/src/Core/Models/Response/CipherResponse.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Api; -using System; +using System; using System.Collections.Generic; +using Bit.Core.Enums; +using Bit.Core.Models.Api; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/ErrorResponse.cs b/src/Core/Models/Response/ErrorResponse.cs index cd240e44c..71cde4317 100644 --- a/src/Core/Models/Response/ErrorResponse.cs +++ b/src/Core/Models/Response/ErrorResponse.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; +using Newtonsoft.Json.Linq; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/IdentityCaptchaResponse.cs b/src/Core/Models/Response/IdentityCaptchaResponse.cs index 018248b94..a5350e8da 100644 --- a/src/Core/Models/Response/IdentityCaptchaResponse.cs +++ b/src/Core/Models/Response/IdentityCaptchaResponse.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using System.Collections.Generic; +using Bit.Core.Enums; using Newtonsoft.Json; -using System.Collections.Generic; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/IdentityResponse.cs b/src/Core/Models/Response/IdentityResponse.cs index 6f693e571..cb843fd8d 100644 --- a/src/Core/Models/Response/IdentityResponse.cs +++ b/src/Core/Models/Response/IdentityResponse.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using Newtonsoft.Json.Linq; namespace Bit.Core.Models.Response diff --git a/src/Core/Models/Response/IdentityTwoFactorResponse.cs b/src/Core/Models/Response/IdentityTwoFactorResponse.cs index 65b1f94b8..928bd4303 100644 --- a/src/Core/Models/Response/IdentityTwoFactorResponse.cs +++ b/src/Core/Models/Response/IdentityTwoFactorResponse.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using System.Collections.Generic; +using Bit.Core.Enums; using Newtonsoft.Json; -using System.Collections.Generic; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/NotificationResponse.cs b/src/Core/Models/Response/NotificationResponse.cs index 8e37b2673..c32812e46 100644 --- a/src/Core/Models/Response/NotificationResponse.cs +++ b/src/Core/Models/Response/NotificationResponse.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; -using System; +using System; using System.Collections.Generic; +using Bit.Core.Enums; namespace Bit.Core.Models.Response { diff --git a/src/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs b/src/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs index c766a82ff..d354f8ae5 100644 --- a/src/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs +++ b/src/Core/Models/Response/OrganizationAutoEnrollStatusResponse.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Response +namespace Bit.Core.Models.Response { public class OrganizationAutoEnrollStatusResponse { diff --git a/src/Core/Models/Response/OrganizationKeysResponse.cs b/src/Core/Models/Response/OrganizationKeysResponse.cs index 28d350f7b..1f9c7aac5 100644 --- a/src/Core/Models/Response/OrganizationKeysResponse.cs +++ b/src/Core/Models/Response/OrganizationKeysResponse.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Models.Response +namespace Bit.Core.Models.Response { public class OrganizationKeysResponse { diff --git a/src/Core/Models/Response/SendResponse.cs b/src/Core/Models/Response/SendResponse.cs index ca6a3bc84..89ee96598 100644 --- a/src/Core/Models/Response/SendResponse.cs +++ b/src/Core/Models/Response/SendResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Core/Models/View/AccountView.cs b/src/Core/Models/View/AccountView.cs new file mode 100644 index 000000000..e89afc511 --- /dev/null +++ b/src/Core/Models/View/AccountView.cs @@ -0,0 +1,41 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.View +{ + public class AccountView : View + { + public AccountView() { } + + public AccountView(Account a = null, bool isActive = false) + { + if (a == null) + { + // null will render as "Add Account" row + return; + } + IsAccount = true; + IsActive = isActive; + UserId = a.Profile?.UserId; + Email = a.Profile?.Email; + Name = a.Profile?.Name; + if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.WebVault)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.WebVault); + } + else if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.Base)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.Base); + } + } + + public bool IsAccount { get; set; } + public AuthenticationStatus? AuthStatus { get; set; } + public bool IsActive { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public string Hostname { get; set; } + } +} diff --git a/src/Core/Models/View/CardView.cs b/src/Core/Models/View/CardView.cs index 421d0b712..6b88d01e3 100644 --- a/src/Core/Models/View/CardView.cs +++ b/src/Core/Models/View/CardView.cs @@ -1,7 +1,7 @@ -using Bit.Core.Models.Domain; -using Bit.Core.Enums; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.RegularExpressions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs index 1d238f271..6bec74ee4 100644 --- a/src/Core/Models/View/CipherView.cs +++ b/src/Core/Models/View/CipherView.cs @@ -1,8 +1,8 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Domain; -using System; +using System; using System.Collections.Generic; using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/FolderView.cs b/src/Core/Models/View/FolderView.cs index e09eea1d3..f20f91107 100644 --- a/src/Core/Models/View/FolderView.cs +++ b/src/Core/Models/View/FolderView.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Domain; -using System; +using System; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/IdentityView.cs b/src/Core/Models/View/IdentityView.cs index d4dae2922..b21a5b927 100644 --- a/src/Core/Models/View/IdentityView.cs +++ b/src/Core/Models/View/IdentityView.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.Domain; +using System.Collections.Generic; using Bit.Core.Enums; -using System.Collections.Generic; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/ItemView.cs b/src/Core/Models/View/ItemView.cs index e0e6fba13..a264cf441 100644 --- a/src/Core/Models/View/ItemView.cs +++ b/src/Core/Models/View/ItemView.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Bit.Core.Enums; namespace Bit.Core.Models.View diff --git a/src/Core/Models/View/LoginUriView.cs b/src/Core/Models/View/LoginUriView.cs index 74440fbaa..62ca2dd39 100644 --- a/src/Core/Models/View/LoginUriView.cs +++ b/src/Core/Models/View/LoginUriView.cs @@ -1,9 +1,9 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Domain; -using Bit.Core.Utilities; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/LoginView.cs b/src/Core/Models/View/LoginView.cs index 595c69928..850082115 100644 --- a/src/Core/Models/View/LoginView.cs +++ b/src/Core/Models/View/LoginView.cs @@ -1,8 +1,8 @@ -using Bit.Core.Models.Domain; -using Bit.Core.Enums; -using System; +using System; using System.Collections.Generic; using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/PasswordHistoryView.cs b/src/Core/Models/View/PasswordHistoryView.cs index 03d646730..0d3472f52 100644 --- a/src/Core/Models/View/PasswordHistoryView.cs +++ b/src/Core/Models/View/PasswordHistoryView.cs @@ -1,5 +1,5 @@ -using Bit.Core.Models.Domain; -using System; +using System; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { @@ -11,7 +11,7 @@ namespace Bit.Core.Models.View { LastUsedDate = ph.LastUsedDate; } - + public string Password { get; set; } public DateTime LastUsedDate { get; set; } } diff --git a/src/Core/Models/View/SecureNoteView.cs b/src/Core/Models/View/SecureNoteView.cs index 67f3818df..88a02fcbe 100644 --- a/src/Core/Models/View/SecureNoteView.cs +++ b/src/Core/Models/View/SecureNoteView.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using System.Collections.Generic; +using Bit.Core.Enums; using Bit.Core.Models.Domain; -using System.Collections.Generic; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/SendFileView.cs b/src/Core/Models/View/SendFileView.cs index 840aa04af..3b1d20ac9 100644 --- a/src/Core/Models/View/SendFileView.cs +++ b/src/Core/Models/View/SendFileView.cs @@ -1,4 +1,4 @@ -using System.Dynamic; +using System.Dynamic; using Bit.Core.Models.Domain; namespace Bit.Core.Models.View diff --git a/src/Core/Models/View/SendTextView.cs b/src/Core/Models/View/SendTextView.cs index f61a8166c..2e391a452 100644 --- a/src/Core/Models/View/SendTextView.cs +++ b/src/Core/Models/View/SendTextView.cs @@ -1,4 +1,4 @@ -using Bit.Core.Models.Domain; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { diff --git a/src/Core/Models/View/SendView.cs b/src/Core/Models/View/SendView.cs index dcc5264e9..259eea531 100644 --- a/src/Core/Models/View/SendView.cs +++ b/src/Core/Models/View/SendView.cs @@ -1,4 +1,4 @@ -using System; +using System; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Utilities; diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index c0b11a6a1..fdd880c59 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -1,4 +1,10 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; @@ -7,12 +13,6 @@ using Bit.Core.Models.Response; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace Bit.Core.Services { @@ -25,12 +25,12 @@ namespace Bit.Core.Services private readonly HttpClient _httpClient = new HttpClient(); private readonly ITokenService _tokenService; private readonly IPlatformUtilsService _platformUtilsService; - private readonly Func _logoutCallbackAsync; + private readonly Func, Task> _logoutCallbackAsync; public ApiService( ITokenService tokenService, IPlatformUtilsService platformUtilsService, - Func logoutCallbackAsync, + Func, Task> logoutCallbackAsync, string customUserAgent = null) { _tokenService = tokenService; @@ -202,7 +202,7 @@ namespace Bit.Core.Services { return SendAsync(HttpMethod.Delete, "/accounts", request, true, false); } - + public Task PostConvertToKeyConnector() { return SendAsync(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false); @@ -438,9 +438,9 @@ namespace Bit.Core.Services } #endregion - + #region Organizations APIs - + public Task GetOrganizationKeysAsync(string id) { return SendAsync(HttpMethod.Get, $"/organizations/{id}/keys", null, true, true); @@ -556,7 +556,7 @@ namespace Bit.Core.Services requestMessage.Method = HttpMethod.Get; requestMessage.RequestUri = new Uri(string.Concat(IdentityBaseUrl, path)); requestMessage.Headers.Add("Accept", "application/json"); - + HttpResponseMessage response; try { @@ -709,7 +709,7 @@ namespace Bit.Core.Services response.StatusCode == HttpStatusCode.Forbidden )) { - await _logoutCallbackAsync(true); + await _logoutCallbackAsync(new Tuple(null, false, true)); return null; } try diff --git a/src/Core/Services/AppIdService.cs b/src/Core/Services/AppIdService.cs index ada27d857..b95fd432f 100644 --- a/src/Core/Services/AppIdService.cs +++ b/src/Core/Services/AppIdService.cs @@ -1,6 +1,6 @@ -using Bit.Core.Abstractions; -using System; +using System; using System.Threading.Tasks; +using Bit.Core.Abstractions; namespace Bit.Core.Services { diff --git a/src/Core/Services/AuditService.cs b/src/Core/Services/AuditService.cs index 67d138672..d6543e9f3 100644 --- a/src/Core/Services/AuditService.cs +++ b/src/Core/Services/AuditService.cs @@ -1,11 +1,11 @@ -using Bit.Core.Abstractions; -using Bit.Core.Exceptions; -using Bit.Core.Models.Response; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Response; namespace Bit.Core.Services { diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index ca76c437f..c1fb38fa8 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -1,11 +1,11 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Bit.Core.Services { @@ -14,13 +14,12 @@ namespace Bit.Core.Services private readonly ICryptoService _cryptoService; private readonly ICryptoFunctionService _cryptoFunctionService; private readonly IApiService _apiService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ITokenService _tokenService; private readonly IAppIdService _appIdService; private readonly II18nService _i18nService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IMessagingService _messagingService; - private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IKeyConnectorService _keyConnectorService; private readonly bool _setCryptoKeys; @@ -30,7 +29,7 @@ namespace Bit.Core.Services ICryptoService cryptoService, ICryptoFunctionService cryptoFunctionService, IApiService apiService, - IUserService userService, + IStateService stateService, ITokenService tokenService, IAppIdService appIdService, II18nService i18nService, @@ -43,13 +42,12 @@ namespace Bit.Core.Services _cryptoService = cryptoService; _cryptoFunctionService = cryptoFunctionService; _apiService = apiService; - _userService = userService; + _stateService = stateService; _tokenService = tokenService; _appIdService = appIdService; _i18nService = i18nService; _platformUtilsService = platformUtilsService; _messagingService = messagingService; - _vaultTimeoutService = vaultTimeoutService; _keyConnectorService = keyConnectorService; _setCryptoKeys = setCryptoKeys; @@ -143,8 +141,12 @@ namespace Bit.Core.Services } public Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, - bool? remember = null) + string captchaToken, bool? remember = null) { + if (captchaToken != null) + { + CaptchaToken = captchaToken; + } return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key, twoFactorProvider, twoFactorToken, remember, CaptchaToken); } @@ -344,6 +346,7 @@ namespace Bit.Core.Services TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2; result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2; CaptchaToken = response.TwoFactorResponse.CaptchaToken; + await _tokenService.ClearTwoFactorTokenAsync(email); return result; } @@ -354,9 +357,26 @@ namespace Bit.Core.Services { await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email); } - await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); - await _userService.SetInformationAsync(_tokenService.GetUserId(), _tokenService.GetEmail(), - tokenResponse.Kdf, tokenResponse.KdfIterations); + await _tokenService.SetAccessTokenAsync(tokenResponse.AccessToken, true); + await _stateService.AddAccountAsync( + new Account( + new Account.AccountProfile() + { + UserId = _tokenService.GetUserId(), + Email = _tokenService.GetEmail(), + Name = _tokenService.GetName(), + KdfType = tokenResponse.Kdf, + KdfIterations = tokenResponse.KdfIterations, + HasPremiumPersonally = _tokenService.GetPremium(), + }, + new Account.AccountTokens() + { + AccessToken = tokenResponse.AccessToken, + RefreshToken = tokenResponse.RefreshToken, + } + ) + ); + _messagingService.Send("accountAdded"); if (_setCryptoKeys) { if (key != null) @@ -430,7 +450,7 @@ namespace Bit.Core.Services } - _vaultTimeoutService.BiometricLocked = false; + await _stateService.SetBiometricLockedAsync(false); _messagingService.Send("loggedIn"); return result; } diff --git a/src/Core/Services/AzureFileUploadService.cs b/src/Core/Services/AzureFileUploadService.cs index 9f39ed8b8..4d18c1246 100644 --- a/src/Core/Services/AzureFileUploadService.cs +++ b/src/Core/Services/AzureFileUploadService.cs @@ -80,63 +80,63 @@ namespace Bit.Core.Services throw new Exception($"Cannot upload file, exceeds maximum size of {blockSize * MAX_BLOCKS_PER_BLOB}"); } - while (blockIndex < numBlocks) - { - uri = await RenewUriIfNecessary(uri, renewalFunc); - var blockUriBuilder = new UriBuilder(uri); - var blockId = EncodeBlockId(blockIndex); - var blockParams = HttpUtility.ParseQueryString(blockUriBuilder.Query); - blockParams.Add("comp", "block"); - blockParams.Add("blockid", blockId); - blockUriBuilder.Query = blockParams.ToString(); - - using (var requestMessage = new HttpRequestMessage()) - { - requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); - requestMessage.Headers.Add("x-ms-version", baseParams["sv"]); - requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob"); - - requestMessage.Content = new ByteArrayContent(data.Buffer.Skip(blockIndex * blockSize).Take(blockSize).ToArray()); - requestMessage.Version = new Version(1, 0); - requestMessage.Method = HttpMethod.Put; - requestMessage.RequestUri = blockUriBuilder.Uri; - - var blockResponse = await _httpClient.SendAsync(requestMessage); - - if (blockResponse.StatusCode != HttpStatusCode.Created) - { - throw new Exception("Failed to create Azure block"); - } - } - - blocksStaged.Add(blockId); - blockIndex++; - } + while (blockIndex < numBlocks) + { + uri = await RenewUriIfNecessary(uri, renewalFunc); + var blockUriBuilder = new UriBuilder(uri); + var blockId = EncodeBlockId(blockIndex); + var blockParams = HttpUtility.ParseQueryString(blockUriBuilder.Query); + blockParams.Add("comp", "block"); + blockParams.Add("blockid", blockId); + blockUriBuilder.Query = blockParams.ToString(); using (var requestMessage = new HttpRequestMessage()) { - uri = await RenewUriIfNecessary(uri, renewalFunc); - var blockListXml = GenerateBlockListXml(blocksStaged); - var blockListUriBuilder = new UriBuilder(uri); - var blockListParams = HttpUtility.ParseQueryString(blockListUriBuilder.Query); - blockListParams.Add("comp", "blocklist"); - blockListUriBuilder.Query = blockListParams.ToString(); - requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); requestMessage.Headers.Add("x-ms-version", baseParams["sv"]); + requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob"); - requestMessage.Content = new StringContent(blockListXml); + requestMessage.Content = new ByteArrayContent(data.Buffer.Skip(blockIndex * blockSize).Take(blockSize).ToArray()); requestMessage.Version = new Version(1, 0); requestMessage.Method = HttpMethod.Put; - requestMessage.RequestUri = blockListUriBuilder.Uri; + requestMessage.RequestUri = blockUriBuilder.Uri; - var blockListResponse = await _httpClient.SendAsync(requestMessage); + var blockResponse = await _httpClient.SendAsync(requestMessage); - if (blockListResponse.StatusCode != HttpStatusCode.Created) + if (blockResponse.StatusCode != HttpStatusCode.Created) { - throw new Exception("Failed to PUT Azure block list"); + throw new Exception("Failed to create Azure block"); } } + + blocksStaged.Add(blockId); + blockIndex++; + } + + using (var requestMessage = new HttpRequestMessage()) + { + uri = await RenewUriIfNecessary(uri, renewalFunc); + var blockListXml = GenerateBlockListXml(blocksStaged); + var blockListUriBuilder = new UriBuilder(uri); + var blockListParams = HttpUtility.ParseQueryString(blockListUriBuilder.Query); + blockListParams.Add("comp", "blocklist"); + blockListUriBuilder.Query = blockListParams.ToString(); + + requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R")); + requestMessage.Headers.Add("x-ms-version", baseParams["sv"]); + + requestMessage.Content = new StringContent(blockListXml); + requestMessage.Version = new Version(1, 0); + requestMessage.Method = HttpMethod.Put; + requestMessage.RequestUri = blockListUriBuilder.Uri; + + var blockListResponse = await _httpClient.SendAsync(requestMessage); + + if (blockListResponse.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Failed to PUT Azure block list"); + } + } } private async Task RenewUriIfNecessary(string uri, Func> renewalFunc) @@ -153,7 +153,7 @@ namespace Bit.Core.Services private string GenerateBlockListXml(List blocksStaged) { var xml = new StringBuilder(""); - foreach(var blockId in blocksStaged) + foreach (var blockId in blocksStaged) { xml.Append($"{blockId}"); } @@ -180,7 +180,7 @@ namespace Bit.Core.Services maxSize = 104857600L; // 100 MiB } - return maxSize > MAX_MOBILE_BLOCK_SIZE ? (int)MAX_MOBILE_BLOCK_SIZE : (int) maxSize; + return maxSize > MAX_MOBILE_BLOCK_SIZE ? (int)MAX_MOBILE_BLOCK_SIZE : (int)maxSize; } private int CompareAzureVersions(string a, string b) diff --git a/src/Core/Services/BitwardenFileUploadService.cs b/src/Core/Services/BitwardenFileUploadService.cs index 52df1a179..88295c0a9 100644 --- a/src/Core/Services/BitwardenFileUploadService.cs +++ b/src/Core/Services/BitwardenFileUploadService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading.Tasks; using Bit.Core.Models.Domain; diff --git a/src/Core/Services/BroadcasterService.cs b/src/Core/Services/BroadcasterService.cs index 09079fb33..553ec49c9 100644 --- a/src/Core/Services/BroadcasterService.cs +++ b/src/Core/Services/BroadcasterService.cs @@ -1,8 +1,8 @@ -using Bit.Core.Abstractions; -using Bit.Core.Models.Domain; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; namespace Bit.App.Services { diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index 5f644472d..bf48e80d8 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -1,4 +1,11 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; @@ -7,27 +14,16 @@ using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Bit.Core.Services { public class CipherService : ICipherService { - private const string Keys_CiphersFormat = "ciphers_{0}"; - private const string Keys_LocalData = "ciphersLocalData"; - private const string Keys_NeverDomains = "neverDomains"; - private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; private List _decryptedCipherCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ISettingsService _settingsService; private readonly IApiService _apiService; private readonly IFileUploadService _fileUploadService; @@ -45,18 +41,18 @@ namespace Bit.Core.Services public CipherService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, ISettingsService settingsService, IApiService apiService, IFileUploadService fileUploadService, IStorageService storageService, II18nService i18nService, Func searchService, - string clearCipherCacheKey, + string clearCipherCacheKey, string[] allClearCipherCacheKeys) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _settingsService = settingsService; _apiService = apiService; _fileUploadService = fileUploadService; @@ -211,11 +207,8 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var localData = await _storageService.GetAsync>>( - Keys_LocalData); - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (!ciphers?.ContainsKey(id) ?? true) { return null; @@ -226,11 +219,8 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var localData = await _storageService.GetAsync>>( - Keys_LocalData); - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); var response = ciphers?.Select(c => new Cipher(c.Value, false, localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null)); return response?.ToList() ?? new List(); @@ -347,7 +337,7 @@ namespace Bit.Core.Services var others = new List(); var ciphers = await ciphersTask; - var defaultMatch = (UriMatchType?)(await _storageService.GetAsync(Constants.DefaultUriMatch)); + var defaultMatch = (UriMatchType?)(await _stateService.GetDefaultUriMatchAsync()); if (defaultMatch == null) { defaultMatch = UriMatchType.Domain; @@ -457,8 +447,7 @@ namespace Bit.Core.Services public async Task UpdateLastUsedDateAsync(string id) { - var ciphersLocalData = await _storageService.GetAsync>>( - Keys_LocalData); + var ciphersLocalData = await _stateService.GetLocalDataAsync(); if (ciphersLocalData == null) { ciphersLocalData = new Dictionary>(); @@ -476,7 +465,7 @@ namespace Bit.Core.Services ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow); } - await _storageService.SaveAsync(Keys_LocalData, ciphersLocalData); + await _stateService.SetLocalDataAsync(ciphersLocalData); // Update cache if (DecryptedCipherCache == null) { @@ -495,13 +484,13 @@ namespace Bit.Core.Services { return; } - var domains = await _storageService.GetAsync>(Keys_NeverDomains); + var domains = await _stateService.GetNeverDomainsAsync(); if (domains == null) { domains = new HashSet(); } domains.Add(domain); - await _storageService.SaveAsync(Keys_NeverDomains, domains); + await _stateService.SetNeverDomainsAsync(domains); } public async Task SaveWithServerAsync(Cipher cipher) @@ -526,7 +515,7 @@ namespace Bit.Core.Services var request = new CipherRequest(cipher); response = await _apiService.PutCipherAsync(cipher.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(data); } @@ -550,7 +539,7 @@ namespace Bit.Core.Services var encCipher = await EncryptAsync(cipher); var request = new CipherShareRequest(encCipher); var response = await _apiService.PutShareCipherAsync(cipher.Id, request); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new CipherData(response, userId, collectionIds); await UpsertAsync(data); } @@ -581,7 +570,7 @@ namespace Bit.Core.Services response = await LegacyServerAttachmentFileUploadAsync(cipher.Id, encFileName, encFileData, orgEncAttachmentKey); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var cData = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(cData); return new Cipher(cData); @@ -602,16 +591,14 @@ namespace Bit.Core.Services { var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList()); await _apiService.PutCipherCollectionsAsync(cipher.Id, request); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = cipher.ToCipherData(userId); await UpsertAsync(data); } public async Task UpsertAsync(CipherData cipher) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(storageKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { ciphers = new Dictionary(); @@ -621,15 +608,13 @@ namespace Bit.Core.Services ciphers.Add(cipher.Id, null); } ciphers[cipher.Id] = cipher; - await _storageService.SaveAsync(storageKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task UpsertAsync(List cipher) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(storageKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { ciphers = new Dictionary(); @@ -642,28 +627,25 @@ namespace Bit.Core.Services } ciphers[c.Id] = c; } - await _storageService.SaveAsync(storageKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task ReplaceAsync(Dictionary ciphers) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_CiphersFormat, userId), ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_CiphersFormat, userId)); + await _stateService.SetEncryptedCiphersAsync(null, userId); await ClearCacheAsync(); } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -673,15 +655,13 @@ namespace Bit.Core.Services return; } ciphers.Remove(id); - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task DeleteAsync(List ids) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -694,7 +674,7 @@ namespace Bit.Core.Services } ciphers.Remove(id); } - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } @@ -706,9 +686,7 @@ namespace Bit.Core.Services public async Task DeleteAttachmentAsync(string id, string attachmentId) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null || !ciphers.ContainsKey(id) || ciphers[id].Attachments == null) { return; @@ -718,7 +696,7 @@ namespace Bit.Core.Services { ciphers[id].Attachments.Remove(attachment); } - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } @@ -771,9 +749,7 @@ namespace Bit.Core.Services public async Task SoftDeleteWithServerAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -785,15 +761,13 @@ namespace Bit.Core.Services await _apiService.PutDeleteCipherAsync(id); ciphers[id].DeletedDate = DateTime.UtcNow; - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task RestoreWithServerAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -805,7 +779,7 @@ namespace Bit.Core.Services var response = await _apiService.PutRestoreCipherAsync(id); ciphers[id].DeletedDate = null; ciphers[id].RevisionDate = response.RevisionDate; - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } diff --git a/src/Core/Services/CollectionService.cs b/src/Core/Services/CollectionService.cs index aadce803e..fe30159dc 100644 --- a/src/Core/Services/CollectionService.cs +++ b/src/Core/Services/CollectionService.cs @@ -1,36 +1,32 @@ -using Bit.Core.Abstractions; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; -using Bit.Core.Models.View; -using Bit.Core.Utilities; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; namespace Bit.Core.Services { public class CollectionService : ICollectionService { - private const string Keys_CollectionsFormat = "collections_{0}"; private const char NestingDelimiter = '/'; private List _decryptedCollectionCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly II18nService _i18nService; public CollectionService( ICryptoService cryptoService, - IUserService userService, - IStorageService storageService, + IStateService stateService, II18nService i18nService) { _cryptoService = cryptoService; - _userService = userService; - _storageService = storageService; + _stateService = stateService; _i18nService = i18nService; } @@ -83,9 +79,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var collections = await _storageService.GetAsync>( - string.Format(Keys_CollectionsFormat, userId)); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (!collections?.ContainsKey(id) ?? true) { return null; @@ -95,9 +89,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var collections = await _storageService.GetAsync>( - string.Format(Keys_CollectionsFormat, userId)); + var collections = await _stateService.GetEncryptedCollectionsAsync(); var response = collections?.Select(c => new Collection(c.Value)); return response?.ToList() ?? new List(); } @@ -148,9 +140,7 @@ namespace Bit.Core.Services public async Task UpsertAsync(CollectionData collection) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(storageKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null) { collections = new Dictionary(); @@ -160,15 +150,13 @@ namespace Bit.Core.Services collections.Add(collection.Id, null); } collections[collection.Id] = collection; - await _storageService.SaveAsync(storageKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task UpsertAsync(List collection) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(storageKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null) { collections = new Dictionary(); @@ -181,34 +169,31 @@ namespace Bit.Core.Services } collections[c.Id] = c; } - await _storageService.SaveAsync(storageKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task ReplaceAsync(Dictionary collections) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_CollectionsFormat, userId), collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_CollectionsFormat, userId)); + await _stateService.SetEncryptedCollectionsAsync(null, userId); _decryptedCollectionCache = null; } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var collectionKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(collectionKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null || !collections.ContainsKey(id)) { return; } collections.Remove(id); - await _storageService.SaveAsync(collectionKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } diff --git a/src/Core/Services/ConsoleLogService.cs b/src/Core/Services/ConsoleLogService.cs index baa292f76..1814d48c6 100644 --- a/src/Core/Services/ConsoleLogService.cs +++ b/src/Core/Services/ConsoleLogService.cs @@ -1,9 +1,9 @@ -using Bit.Core.Abstractions; -using System; +using System; +using Bit.Core.Abstractions; namespace Bit.Core.Services { - public class ConsoleLogService : ILogService + public class ConsoleLogService : INativeLogService { public void Debug(string message) { diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs index 375f6d467..5cd5312eb 100644 --- a/src/Core/Services/CryptoService.cs +++ b/src/Core/Services/CryptoService.cs @@ -1,24 +1,22 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Response; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Numerics; using Bit.Core.Utilities; namespace Bit.Core.Services { public class CryptoService : ICryptoService { - private readonly IStorageService _storageService; - private readonly IStorageService _secureStorageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; - private SymmetricCryptoKey _key; private SymmetricCryptoKey _encKey; private SymmetricCryptoKey _legacyEtmKey; private string _keyHash; @@ -28,39 +26,31 @@ namespace Bit.Core.Services private Task _getEncKeysTask; private Task> _getOrgKeysTask; - private const string Keys_Key = "key"; - private const string Keys_EncOrgKeys = "encOrgKeys"; - private const string Keys_EncPrivateKey = "encPrivateKey"; - private const string Keys_EncKey = "encKey"; - private const string Keys_KeyHash = "keyHash"; - public CryptoService( - IStorageService storageService, - IStorageService secureStorageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService) { - _storageService = storageService; - _secureStorageService = secureStorageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; } public async Task SetKeyAsync(SymmetricCryptoKey key) { - _key = key; - var option = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var biometric = await _storageService.GetAsync(Constants.BiometricUnlockKey); + await _stateService.SetKeyDecryptedAsync(key); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); if (option.HasValue && !biometric.GetValueOrDefault()) { // If we have a lock option set, we do not store the key return; } - await _secureStorageService.SaveAsync(Keys_Key, key?.KeyB64); + await _stateService.SetKeyEncryptedAsync(key?.KeyB64); } public async Task SetKeyHashAsync(string keyHash) { _keyHash = keyHash; - await _storageService.SaveAsync(Keys_KeyHash, keyHash); + await _stateService.SetKeyHashAsync(keyHash); } public async Task SetEncKeyAsync(string encKey) @@ -69,7 +59,7 @@ namespace Bit.Core.Services { return; } - await _storageService.SaveAsync(Keys_EncKey, encKey); + await _stateService.SetEncKeyEncryptedAsync(encKey); _encKey = null; } @@ -79,7 +69,7 @@ namespace Bit.Core.Services { return; } - await _storageService.SaveAsync(Keys_EncPrivateKey, encPrivateKey); + await _stateService.SetPrivateKeyEncryptedAsync(encPrivateKey); _privateKey = null; } @@ -87,21 +77,23 @@ namespace Bit.Core.Services { var orgKeys = orgs.ToDictionary(org => org.Id, org => org.Key); _orgKeys = null; - await _storageService.SaveAsync(Keys_EncOrgKeys, orgKeys); + await _stateService.SetOrgKeysEncryptedAsync(orgKeys); } - public async Task GetKeyAsync() + public async Task GetKeyAsync(string userId = null) { - if (_key != null) + var inMemoryKey = await _stateService.GetKeyDecryptedAsync(userId); + if (inMemoryKey != null) { - return _key; + return inMemoryKey; } - var key = await _secureStorageService.GetAsync(Keys_Key); + var key = await _stateService.GetKeyEncryptedAsync(userId); if (key != null) { - _key = new SymmetricCryptoKey(Convert.FromBase64String(key)); + inMemoryKey = new SymmetricCryptoKey(Convert.FromBase64String(key)); + await _stateService.SetKeyDecryptedAsync(inMemoryKey, userId); } - return _key; + return inMemoryKey; } public async Task GetKeyHashAsync() @@ -110,7 +102,7 @@ namespace Bit.Core.Services { return _keyHash; } - var keyHash = await _storageService.GetAsync(Keys_KeyHash); + var keyHash = await _stateService.GetKeyHashAsync(); if (keyHash != null) { _keyHash = keyHash; @@ -132,7 +124,7 @@ namespace Bit.Core.Services { try { - var encKey = await _storageService.GetAsync(Keys_EncKey); + var encKey = await _stateService.GetEncKeyEncryptedAsync(); if (encKey == null) { return null; @@ -200,7 +192,7 @@ namespace Bit.Core.Services { return _privateKey; } - var encPrivateKey = await _storageService.GetAsync(Keys_EncPrivateKey); + var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync(); if (encPrivateKey == null) { return null; @@ -238,7 +230,7 @@ namespace Bit.Core.Services { try { - var encOrgKeys = await _storageService.GetAsync>(Keys_EncOrgKeys); + var encOrgKeys = await _stateService.GetOrgKeysEncryptedAsync(); if (encOrgKeys == null) { return null; @@ -303,84 +295,95 @@ namespace Bit.Core.Services return false; } - public async Task HasKeyAsync() + public async Task HasKeyAsync(string userId = null) { - var key = await GetKeyAsync(); + var key = await GetKeyAsync(userId); return key != null; } public async Task HasEncKeyAsync() { - var encKey = await _storageService.GetAsync(Keys_EncKey); + var encKey = await _stateService.GetEncKeyEncryptedAsync(); return encKey != null; } - public async Task ClearKeyAsync() + public async Task ClearKeyAsync(string userId = null) { - _key = _legacyEtmKey = null; - await _secureStorageService.RemoveAsync(Keys_Key); + await _stateService.SetKeyDecryptedAsync(null, userId); + _legacyEtmKey = null; + await _stateService.SetKeyEncryptedAsync(null, userId); } - public async Task ClearKeyHashAsync() + public async Task ClearKeyHashAsync(string userId = null) { _keyHash = null; - await _storageService.RemoveAsync(Keys_KeyHash); + await _stateService.SetKeyHashAsync(null, userId); } - public async Task ClearEncKeyAsync(bool memoryOnly = false) + public async Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null) { _encKey = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncKey); + await _stateService.SetEncKeyEncryptedAsync(null, userId); } } - public async Task ClearKeyPairAsync(bool memoryOnly = false) + public async Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null) { _publicKey = _privateKey = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncPrivateKey); + await _stateService.SetPrivateKeyEncryptedAsync(null, userId); } } - public async Task ClearOrgKeysAsync(bool memoryOnly = false) + public async Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null) { _orgKeys = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncOrgKeys); + await _stateService.SetOrgKeysEncryptedAsync(null, userId); } } - public async Task ClearPinProtectedKeyAsync() + public async Task ClearPinProtectedKeyAsync(string userId = null) { - await _storageService.RemoveAsync(Constants.PinProtectedKey); + await _stateService.SetPinProtectedAsync(null, userId); } - public async Task ClearKeysAsync() + public void ClearCache() + { + _encKey = null; + _legacyEtmKey = null; + _keyHash = null; + _publicKey = null; + _privateKey = null; + _orgKeys = null; + } + + public async Task ClearKeysAsync(string userId = null) { await Task.WhenAll(new Task[] { - ClearKeyAsync(), - ClearKeyHashAsync(), - ClearOrgKeysAsync(), - ClearEncKeyAsync(), - ClearKeyPairAsync(), - ClearPinProtectedKeyAsync() + ClearKeyAsync(userId), + ClearKeyHashAsync(userId), + ClearOrgKeysAsync(false, userId), + ClearEncKeyAsync(false, userId), + ClearKeyPairAsync(false, userId), + ClearPinProtectedKeyAsync(userId) }); } public async Task ToggleKeyAsync() { var key = await GetKeyAsync(); - var option = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var biometric = await _storageService.GetAsync(Constants.BiometricUnlockKey); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); if (!biometric.GetValueOrDefault() && (option != null || option == 0)) { await ClearKeyAsync(); - _key = key; + await _stateService.SetKeyDecryptedAsync(key); return; } await SetKeyAsync(key); @@ -415,7 +418,7 @@ namespace Bit.Core.Services { if (protectedKeyCs == null) { - var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + var pinProtectedKey = await _stateService.GetPinProtectedAsync(); if (pinProtectedKey == null) { throw new Exception("No PIN protected key found."); diff --git a/src/Core/Services/EnvironmentService.cs b/src/Core/Services/EnvironmentService.cs index 315e27126..a2479c87f 100644 --- a/src/Core/Services/EnvironmentService.cs +++ b/src/Core/Services/EnvironmentService.cs @@ -1,22 +1,25 @@ -using Bit.Core.Abstractions; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; -using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Bit.Core.Services { public class EnvironmentService : IEnvironmentService { + private const string DEFAULT_WEB_VAULT_URL = "https://vault.bitwarden.com"; + private const string DEFAULT_WEB_SEND_URL = "https://send.bitwarden.com/#"; + private readonly IApiService _apiService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; public EnvironmentService( IApiService apiService, - IStorageService storageService) + IStateService stateService) { _apiService = apiService; - _storageService = storageService; + _stateService = stateService; } public string BaseUrl { get; set; } @@ -27,22 +30,33 @@ namespace Bit.Core.Services public string NotificationsUrl { get; set; } public string EventsUrl { get; set; } - public string GetWebVaultUrl() + public string GetWebVaultUrl(bool returnNullIfDefault = false) { if (!string.IsNullOrWhiteSpace(WebVaultUrl)) { return WebVaultUrl; } - else if (!string.IsNullOrWhiteSpace(BaseUrl)) + + if (!string.IsNullOrWhiteSpace(BaseUrl)) { return BaseUrl; } - return null; + + return returnNullIfDefault ? (string)null : DEFAULT_WEB_VAULT_URL; + } + + public string GetWebSendUrl() + { + return GetWebVaultUrl(true) is string webVaultUrl ? $"{webVaultUrl}/#/send/" : DEFAULT_WEB_SEND_URL; } public async Task SetUrlsFromStorageAsync() { - var urls = await _storageService.GetAsync(Constants.EnvironmentUrlsKey); + var urls = await _stateService.GetEnvironmentUrlsAsync(); + if (urls == null) + { + urls = await _stateService.GetPreAuthEnvironmentUrlsAsync(); + } if (urls == null) { urls = new EnvironmentUrlData(); @@ -54,6 +68,7 @@ namespace Bit.Core.Services _apiService.SetUrls(envUrls); return; } + BaseUrl = urls.Base; WebVaultUrl = urls.WebVault; ApiUrl = envUrls.Api = urls.Api; IdentityUrl = envUrls.Identity = urls.Identity; @@ -72,7 +87,7 @@ namespace Bit.Core.Services urls.Icons = FormatUrl(urls.Icons); urls.Notifications = FormatUrl(urls.Notifications); urls.Events = FormatUrl(urls.Events); - await _storageService.SaveAsync(Constants.EnvironmentUrlsKey, urls); + await _stateService.SetPreAuthEnvironmentUrlsAsync(urls); BaseUrl = urls.Base; WebVaultUrl = urls.WebVault; ApiUrl = urls.Api; diff --git a/src/Core/Services/EventService.cs b/src/Core/Services/EventService.cs index 3a599d17a..45f566f84 100644 --- a/src/Core/Services/EventService.cs +++ b/src/Core/Services/EventService.cs @@ -1,42 +1,42 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Request; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Bit.Core.Services { public class EventService : IEventService { - private readonly IStorageService _storageService; private readonly IApiService _apiService; - private readonly IUserService _userService; + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; private readonly ICipherService _cipherService; public EventService( - IStorageService storageService, IApiService apiService, - IUserService userService, + IStateService stateService, + IOrganizationService organizationService, ICipherService cipherService) { - _storageService = storageService; _apiService = apiService; - _userService = userService; + _stateService = stateService; + _organizationService = organizationService; _cipherService = cipherService; } public async Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false) { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; } - var organizations = await _userService.GetAllOrganizationAsync(); + var organizations = await _organizationService.GetAllAsync(); if (organizations == null) { return; @@ -54,7 +54,7 @@ namespace Bit.Core.Services return; } } - var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + var eventCollection = await _stateService.GetEventCollectionAsync(); if (eventCollection == null) { eventCollection = new List(); @@ -65,7 +65,7 @@ namespace Bit.Core.Services CipherId = cipherId, Date = DateTime.UtcNow }); - await _storageService.SaveAsync(Constants.EventCollectionKey, eventCollection); + await _stateService.SetEventCollectionAsync(eventCollection); if (uploadImmediately) { await UploadEventsAsync(); @@ -74,12 +74,12 @@ namespace Bit.Core.Services public async Task UploadEventsAsync() { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; } - var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + var eventCollection = await _stateService.GetEventCollectionAsync(); if (eventCollection == null || !eventCollection.Any()) { return; @@ -100,7 +100,7 @@ namespace Bit.Core.Services public async Task ClearEventsAsync() { - await _storageService.RemoveAsync(Constants.EventCollectionKey); + await _stateService.SetEventCollectionAsync(null); } } } diff --git a/src/Core/Services/FileUploadService.cs b/src/Core/Services/FileUploadService.cs index 28856ed96..fbc04acd7 100644 --- a/src/Core/Services/FileUploadService.cs +++ b/src/Core/Services/FileUploadService.cs @@ -1,11 +1,12 @@ +using System; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Response; -using Bit.Core.Enums; -using System; -namespace Bit.Core.Services { +namespace Bit.Core.Services +{ public class FileUploadService : IFileUploadService { public FileUploadService(ApiService apiService) @@ -41,7 +42,8 @@ namespace Bit.Core.Services { default: throw new Exception($"Unkown file upload type: {uploadData.FileUploadType}"); } - } catch + } + catch { await _apiService.DeleteCipherAttachmentAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId); throw; diff --git a/src/Core/Services/FolderService.cs b/src/Core/Services/FolderService.cs index 6eb38ef23..044ee2b93 100644 --- a/src/Core/Services/FolderService.cs +++ b/src/Core/Services/FolderService.cs @@ -1,44 +1,39 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Bit.Core.Services { public class FolderService : IFolderService { - private const string Keys_CiphersFormat = "ciphers_{0}"; - private const string Keys_FoldersFormat = "folders_{0}"; private const char NestingDelimiter = '/'; private List _decryptedFolderCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; - private readonly IStorageService _storageService; private readonly II18nService _i18nService; private readonly ICipherService _cipherService; public FolderService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IApiService apiService, - IStorageService storageService, II18nService i18nService, ICipherService cipherService) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _apiService = apiService; - _storageService = storageService; _i18nService = i18nService; _cipherService = cipherService; } @@ -60,9 +55,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var folders = await _storageService.GetAsync>( - string.Format(Keys_FoldersFormat, userId)); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (!folders?.ContainsKey(id) ?? true) { return null; @@ -72,9 +65,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var folders = await _storageService.GetAsync>( - string.Format(Keys_FoldersFormat, userId)); + var folders = await _stateService.GetEncryptedFoldersAsync(); var response = folders?.Select(f => new Folder(f.Value)); return response?.ToList() ?? new List(); } @@ -116,9 +107,12 @@ namespace Bit.Core.Services return _decryptedFolderCache; } - public async Task>> GetAllNestedAsync() + public async Task>> GetAllNestedAsync(List folders = null) { - var folders = await GetAllDecryptedAsync(); + if (folders == null) + { + folders = await GetAllDecryptedAsync(); + } var nodes = new List>(); foreach (var f in folders) { @@ -153,16 +147,14 @@ namespace Bit.Core.Services { response = await _apiService.PutFolderAsync(folder.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new FolderData(response, userId); await UpsertAsync(data); } public async Task UpsertAsync(FolderData folder) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(storageKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null) { folders = new Dictionary(); @@ -172,15 +164,13 @@ namespace Bit.Core.Services folders.Add(folder.Id, null); } folders[folder.Id] = folder; - await _storageService.SaveAsync(storageKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task UpsertAsync(List folder) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(storageKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null) { folders = new Dictionary(); @@ -193,39 +183,35 @@ namespace Bit.Core.Services } folders[f.Id] = f; } - await _storageService.SaveAsync(storageKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task ReplaceAsync(Dictionary folders) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_FoldersFormat, userId), folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_FoldersFormat, userId)); + await _stateService.SetEncryptedFoldersAsync(null, userId); _decryptedFolderCache = null; } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var folderKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(folderKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null || !folders.ContainsKey(id)) { return; } folders.Remove(id); - await _storageService.SaveAsync(folderKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; // Items in a deleted folder are re-assigned to "No Folder" - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers != null) { var updates = new List(); diff --git a/src/Core/Services/InMemoryStorageService.cs b/src/Core/Services/InMemoryStorageService.cs index fa15fc71b..164565dab 100644 --- a/src/Core/Services/InMemoryStorageService.cs +++ b/src/Core/Services/InMemoryStorageService.cs @@ -1,7 +1,7 @@ -using Bit.Core.Abstractions; -using Newtonsoft.Json; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json; namespace Bit.Core.Services { diff --git a/src/Core/Services/KeyConnectorService.cs b/src/Core/Services/KeyConnectorService.cs index 733d79e78..dbeb8cd06 100644 --- a/src/Core/Services/KeyConnectorService.cs +++ b/src/Core/Services/KeyConnectorService.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Bit.Core.Abstractions; -using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; @@ -9,24 +8,20 @@ namespace Bit.Core.Services { public class KeyConnectorService : IKeyConnectorService { - private const string Keys_UsesKeyConnector = "usesKeyConnector"; - - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ICryptoService _cryptoService; - private readonly IStorageService _storageService; private readonly ITokenService _tokenService; private readonly IApiService _apiService; + private readonly IOrganizationService _organizationService; - private bool? _usesKeyConnector; - - public KeyConnectorService(IUserService userService, ICryptoService cryptoService, - IStorageService storageService, ITokenService tokenService, IApiService apiService) + public KeyConnectorService(IStateService stateService, ICryptoService cryptoService, + ITokenService tokenService, IApiService apiService, OrganizationService organizationService) { - _userService = userService; + _stateService = stateService; _cryptoService = cryptoService; - _storageService = storageService; _tokenService = tokenService; _apiService = apiService; + _organizationService = organizationService; } public async Task GetAndSetKey(string url) @@ -41,28 +36,22 @@ namespace Bit.Core.Services catch (Exception e) { throw new Exception("Unable to reach Key Connector", e); - } + } } public async Task SetUsesKeyConnector(bool usesKeyConnector) { - _usesKeyConnector = usesKeyConnector; - await _storageService.SaveAsync(Keys_UsesKeyConnector, usesKeyConnector); + await _stateService.SetUsesKeyConnectorAsync(usesKeyConnector); } public async Task GetUsesKeyConnector() { - if (!_usesKeyConnector.HasValue) - { - _usesKeyConnector = await _storageService.GetAsync(Keys_UsesKeyConnector); - } - - return _usesKeyConnector.Value; + return await _stateService.GetUsesKeyConnectorAsync(); } public async Task GetManagingOrganization() { - var orgs = await _userService.GetAllOrganizationAsync(); + var orgs = await _organizationService.GetAllAsync(); return orgs.Find(o => o.UsesKeyConnector && !o.IsAdmin); @@ -88,7 +77,7 @@ namespace Bit.Core.Services public async Task UserNeedsMigration() { - var loggedInUsingSso = _tokenService.GetIsExternal(); + var loggedInUsingSso = await _tokenService.GetIsExternal(); var requiredByOrganization = await GetManagingOrganization() != null; var userIsNotUsingKeyConnector = !await GetUsesKeyConnector(); diff --git a/src/Core/Services/LiteDbStorageService.cs b/src/Core/Services/LiteDbStorageService.cs index 35a450536..48f1325ba 100644 --- a/src/Core/Services/LiteDbStorageService.cs +++ b/src/Core/Services/LiteDbStorageService.cs @@ -1,9 +1,9 @@ -using Bit.Core.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using LiteDB; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System.Linq; -using System.Threading.Tasks; namespace Bit.Core.Services { diff --git a/src/Core/Services/Logging/DebugLogger.cs b/src/Core/Services/Logging/DebugLogger.cs new file mode 100644 index 000000000..a52c1de5d --- /dev/null +++ b/src/Core/Services/Logging/DebugLogger.cs @@ -0,0 +1,57 @@ +#if !FDROID +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + public class DebugLogger : ILogger + { + static ILogger _instance; + public static ILogger Instance + { + get + { + if (_instance is null) + { + _instance = new DebugLogger(); + } + return _instance; + } + } + + protected DebugLogger() + { + } + + public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; + var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; + + if (string.IsNullOrEmpty(message)) + { + Debug.WriteLine($"Error found in: {classAndMethod})"); + return; + } + + Debug.WriteLine($"File: {filePathAndLineNumber}"); + Debug.WriteLine($"Method: {memberName}"); + Debug.WriteLine($"Message: {message}"); + + } + + public void Exception(Exception ex) => Debug.WriteLine(ex); + + public Task InitAsync() => Task.CompletedTask; + + public Task IsEnabled() => Task.FromResult(true); + + public Task SetEnabled(bool value) => Task.CompletedTask; + } +} +#endif diff --git a/src/Core/Services/Logging/Logger.cs b/src/Core/Services/Logging/Logger.cs new file mode 100644 index 000000000..d22af6df5 --- /dev/null +++ b/src/Core/Services/Logging/Logger.cs @@ -0,0 +1,136 @@ +#if !FDROID +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Microsoft.AppCenter; +using Microsoft.AppCenter.Crashes; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class Logger : ILogger + { + private const string iOSAppSecret = "51f96ae5-68ba-45f6-99a1-8ad9f63046c3"; + private const string DroidAppSecret = "d3834185-b4a6-4347-9047-b86c65293d42"; + + private string _userId; + private string _appId; + private bool _isInitialised = false; + + static ILogger _instance; + public static ILogger Instance + { + get + { + if (_instance is null) + { + _instance = new Logger(); + } + return _instance; + } + } + + protected Logger() + { + } + + + public string Description + { + get + { + return JsonConvert.SerializeObject(new + { + AppId = _appId, + UserId = _userId + }, Formatting.Indented); + } + } + + public async Task InitAsync() + { + if (_isInitialised) + { + return; + } + + var device = ServiceContainer.Resolve("platformUtilsService").GetDevice(); + _userId = await ServiceContainer.Resolve("stateService").GetActiveUserIdAsync(); + _appId = await ServiceContainer.Resolve("appIdService").GetAppIdAsync(); + + switch (device) + { + case Enums.DeviceType.Android: + AppCenter.Start(DroidAppSecret, typeof(Crashes)); + break; + case Enums.DeviceType.iOS: + AppCenter.Start(iOSAppSecret, typeof(Crashes)); + break; + default: + throw new AppCenterException("Cannot start AppCenter. Device type is not configured."); + + } + + AppCenter.SetUserId(_userId); + + Crashes.GetErrorAttachments = (ErrorReport report) => + { + return new ErrorAttachmentLog[] + { + ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"), + }; + }; + + _isInitialised = true; + } + + public async Task IsEnabled() => await AppCenter.IsEnabledAsync(); + + public async Task SetEnabled(bool value) => await AppCenter.SetEnabledAsync(value); + + public void Error(string message, + IDictionary extraData = null, + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) + { + var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; + var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; + var properties = new Dictionary + { + ["File"] = filePathAndLineNumber, + ["Method"] = memberName + }; + + var exception = new Exception(message ?? $"Error found in: {classAndMethod}"); + if (extraData == null) + { + Crashes.TrackError(exception, properties); + } + else + { + var data = properties.Concat(extraData).ToDictionary(x => x.Key, x => x.Value); + Crashes.TrackError(exception, data); + } + } + + public void Exception(Exception exception) + { + try + { + Crashes.TrackError(exception); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + } +} +#endif diff --git a/src/Core/Services/Logging/LoggerHelper.cs b/src/Core/Services/Logging/LoggerHelper.cs new file mode 100644 index 000000000..9cdf225f6 --- /dev/null +++ b/src/Core/Services/Logging/LoggerHelper.cs @@ -0,0 +1,31 @@ +using System; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public static class LoggerHelper + { + /// + /// Logs the exception even if the service can't be resolved. + /// Useful when we need to log an exception in situations where the ServiceContainer may not be initialized. + /// + /// + public static void LogEvenIfCantBeResolved(Exception ex) + { + if (ServiceContainer.Resolve("logger", true) is ILogger logger) + { + logger.Exception(ex); + } + else + { +#if !FDROID + // just in case the caller throws the exception in a moment where the logger can't be resolved + // we need to track the error as well + Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); +#endif + + } + } + } +} diff --git a/src/Core/Services/Logging/StubLogger.cs b/src/Core/Services/Logging/StubLogger.cs new file mode 100644 index 000000000..2bb8bcd45 --- /dev/null +++ b/src/Core/Services/Logging/StubLogger.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.Core.Services +{ + /// + /// A logger that does nothing, this is useful on e.g. FDroid, where we cannot use logging through AppCenter + /// + public class StubLogger : ILogger + { + public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + } + + public void Exception(Exception ex) + { + } + + public Task InitAsync() => Task.CompletedTask; + + public Task IsEnabled() => Task.FromResult(false); + + public Task SetEnabled(bool value) => Task.CompletedTask; + } +} diff --git a/src/Core/Services/OrganizationService.cs b/src/Core/Services/OrganizationService.cs new file mode 100644 index 000000000..8f25c8076 --- /dev/null +++ b/src/Core/Services/OrganizationService.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Services +{ + public class OrganizationService : IOrganizationService + { + private readonly IStateService _stateService; + + public OrganizationService(IStateService stateService) + { + _stateService = stateService; + } + + public async Task GetAsync(string id) + { + var organizations = await _stateService.GetOrganizationsAsync(); + if (organizations == null || !organizations.ContainsKey(id)) + { + return null; + } + return new Organization(organizations[id]); + } + + public async Task GetByIdentifierAsync(string identifier) + { + var organizations = await GetAllAsync(); + if (organizations == null || organizations.Count == 0) + { + return null; + } + return organizations.FirstOrDefault(o => o.Identifier == identifier); + } + + public async Task> GetAllAsync(string userId = null) + { + var organizations = await _stateService.GetOrganizationsAsync(userId); + return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); + } + + public async Task ReplaceAsync(Dictionary organizations) + { + await _stateService.SetOrganizationsAsync(organizations); + } + + public async Task ClearAllAsync(string userId) + { + await _stateService.SetOrganizationsAsync(null, userId); + } + } +} diff --git a/src/Core/Services/PasswordGenerationService.cs b/src/Core/Services/PasswordGenerationService.cs index 746d9b345..b82152e97 100644 --- a/src/Core/Services/PasswordGenerationService.cs +++ b/src/Core/Services/PasswordGenerationService.cs @@ -1,21 +1,19 @@ -using Bit.Core.Abstractions; -using Bit.Core.Models.Domain; -using Bit.Core.Utilities; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; using Zxcvbn; namespace Bit.Core.Services { public class PasswordGenerationService : IPasswordGenerationService { - private const string Keys_Options = "passwordGenerationOptions"; - private const string Keys_History = "generatedPasswordHistory"; private const int MaxPasswordsInHistory = 100; private const string LowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; private const string UppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; @@ -23,7 +21,7 @@ namespace Bit.Core.Services private const string SpecialCharSet = "!@#$%^&*"; private readonly ICryptoService _cryptoService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; private readonly IPolicyService _policyService; private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true); @@ -32,12 +30,12 @@ namespace Bit.Core.Services public PasswordGenerationService( ICryptoService cryptoService, - IStorageService storageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService, IPolicyService policyService) { _cryptoService = cryptoService; - _storageService = storageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; _policyService = policyService; } @@ -95,7 +93,7 @@ namespace Bit.Core.Services // Build out other character sets var allCharSet = string.Empty; var lowercaseCharSet = LowercaseCharSet; - if (options.Ambiguous.GetValueOrDefault()) + if (options.AllowAmbiguousChar.GetValueOrDefault()) { lowercaseCharSet = string.Concat(lowercaseCharSet, "l"); } @@ -105,7 +103,7 @@ namespace Bit.Core.Services } var uppercaseCharSet = UppercaseCharSet; - if (options.Ambiguous.GetValueOrDefault()) + if (options.AllowAmbiguousChar.GetValueOrDefault()) { uppercaseCharSet = string.Concat(uppercaseCharSet, "IO"); } @@ -115,7 +113,7 @@ namespace Bit.Core.Services } var numberCharSet = NumberCharSet; - if (options.Ambiguous.GetValueOrDefault()) + if (options.AllowAmbiguousChar.GetValueOrDefault()) { numberCharSet = string.Concat(numberCharSet, "01"); } @@ -160,6 +158,12 @@ namespace Bit.Core.Services return password.ToString(); } + public void ClearCache() + { + _optionsCache = null; + _history = null; + } + public async Task GeneratePassphraseAsync(PasswordGenerationOptions options) { options.Merge(_defaultOptions); @@ -204,7 +208,7 @@ namespace Bit.Core.Services { if (_optionsCache == null) { - var options = await _storageService.GetAsync(Keys_Options); + var options = await _stateService.GetPasswordGenerationOptionsAsync(); if (options == null) { _optionsCache = _defaultOptions; @@ -380,7 +384,7 @@ namespace Bit.Core.Services { enforcedOptions.Capitalize = true; } - + var includeNumber = GetPolicyBool(currentPolicy, "includeNumber"); if (includeNumber != null && (bool)includeNumber) { @@ -416,7 +420,7 @@ namespace Bit.Core.Services } return null; } - + private string GetPolicyString(Policy policy, string key) { if (policy.Data.ContainsKey(key)) @@ -432,7 +436,7 @@ namespace Bit.Core.Services public async Task SaveOptionsAsync(PasswordGenerationOptions options) { - await _storageService.SaveAsync(Keys_Options, options); + await _stateService.SetPasswordGenerationOptionsAsync(options); _optionsCache = options; } @@ -445,7 +449,7 @@ namespace Bit.Core.Services } if (_history == null) { - var encrypted = await _storageService.GetAsync>(Keys_History); + var encrypted = await _stateService.GetEncryptedPasswordGenerationHistory(); _history = await DecryptHistoryAsync(encrypted); } return _history ?? new List(); @@ -473,13 +477,13 @@ namespace Bit.Core.Services } var newHistory = await EncryptHistoryAsync(currentHistory); token.ThrowIfCancellationRequested(); - await _storageService.SaveAsync(Keys_History, newHistory); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(newHistory); } - public async Task ClearAsync() + public async Task ClearAsync(string userId = null) { _history = new List(); - await _storageService.RemoveAsync(Keys_History); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(null, userId); } public Result PasswordStrength(string password, List userInputs = null) @@ -592,7 +596,7 @@ namespace Bit.Core.Services { options.WordSeparator = options.WordSeparator[0].ToString(); } - + SanitizePasswordLength(options, false); } @@ -679,13 +683,13 @@ namespace Bit.Core.Services if (options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() <= 0) { minUppercaseCalc = 1; - } + } else if (!options.Uppercase.GetValueOrDefault()) { minUppercaseCalc = 0; } - if (options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() <= 0) + if (options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() <= 0) { minLowercaseCalc = 1; } @@ -697,7 +701,7 @@ namespace Bit.Core.Services if (options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() <= 0) { minNumberCalc = 1; - } + } else if (!options.Number.GetValueOrDefault()) { minNumberCalc = 0; @@ -706,7 +710,7 @@ namespace Bit.Core.Services if (options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() <= 0) { minSpecialCalc = 1; - } + } else if (!options.Special.GetValueOrDefault()) { minSpecialCalc = 0; diff --git a/src/Core/Services/PclCryptoFunctionService.cs b/src/Core/Services/PclCryptoFunctionService.cs index 8299f8218..fa8e8693d 100644 --- a/src/Core/Services/PclCryptoFunctionService.cs +++ b/src/Core/Services/PclCryptoFunctionService.cs @@ -1,10 +1,10 @@ -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using PCLCrypto; -using System; +using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using PCLCrypto; using static PCLCrypto.WinRTCrypto; namespace Bit.Core.Services diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index 95529ab26..1f15e192c 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -12,33 +12,29 @@ namespace Bit.Core.Services { public class PolicyService : IPolicyService { - private const string Keys_PoliciesPrefix = "policies_{0}"; - - private readonly IStorageService _storageService; - private readonly IUserService _userService; + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; private IEnumerable _policyCache; public PolicyService( - IStorageService storageService, - IUserService userService) + IStateService stateService, + IOrganizationService organizationService) { - _storageService = storageService; - _userService = userService; + _stateService = stateService; + _organizationService = organizationService; } - + public void ClearCache() { _policyCache = null; } - public async Task> GetAll(PolicyType? type) + public async Task> GetAll(PolicyType? type, string userId = null) { if (_policyCache == null) { - var userId = await _userService.GetUserIdAsync(); - var policies = await _storageService.GetAsync>( - string.Format(Keys_PoliciesPrefix, userId)); + var policies = await _stateService.GetEncryptedPoliciesAsync(userId); if (policies == null) { return null; @@ -56,27 +52,26 @@ namespace Bit.Core.Services } } - public async Task Replace(Dictionary policies) + public async Task Replace(Dictionary policies, string userId = null) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_PoliciesPrefix, userId), policies); + await _stateService.SetEncryptedPoliciesAsync(policies, userId); _policyCache = null; } - public async Task Clear(string userId) + public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_PoliciesPrefix, userId)); + await _stateService.SetEncryptedPoliciesAsync(null, userId); _policyCache = null; } public async Task GetMasterPasswordPolicyOptions( - IEnumerable policies = null) + IEnumerable policies = null, string userId = null) { MasterPasswordPolicyOptions enforcedOptions = null; if (policies == null) { - policies = await GetAll(PolicyType.MasterPassword); + policies = await GetAll(PolicyType.MasterPassword, userId); } else { @@ -198,14 +193,15 @@ namespace Bit.Core.Services return new Tuple(resetPasswordPolicyOptions, policy != null); } - public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter) + public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, + string userId = null) { - var policies = await GetAll(policyType); + var policies = await GetAll(policyType, userId); if (policies == null) { return false; } - var organizations = await _userService.GetAllOrganizationAsync(); + var organizations = await _organizationService.GetAllAsync(userId); IEnumerable filteredPolicies; @@ -251,6 +247,13 @@ namespace Bit.Core.Services return null; } + public async Task ShouldShowVaultFilterAsync() + { + var organizations = await _organizationService.GetAllAsync(); + var personalOwnershipPolicyApplies = await PolicyAppliesToUser(PolicyType.PersonalOwnership); + return (organizations?.Any() ?? false) && !personalOwnershipPolicyApplies; + } + private bool? GetPolicyBool(Policy policy, string key) { if (policy.Data.ContainsKey(key)) diff --git a/src/Core/Services/SearchService.cs b/src/Core/Services/SearchService.cs index 689fbf59a..8a5b6ac7d 100644 --- a/src/Core/Services/SearchService.cs +++ b/src/Core/Services/SearchService.cs @@ -1,10 +1,10 @@ -using Bit.Core.Abstractions; -using Bit.Core.Models.View; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; namespace Bit.Core.Services { @@ -74,28 +74,34 @@ namespace Bit.Core.Services CancellationToken ct = default, bool deleted = false) { ct.ThrowIfCancellationRequested(); + var matchedCiphers = new List(); + var lowPriorityMatchedCiphers = new List(); query = query.Trim().ToLower(); - return ciphers.Where(c => + + foreach (var c in ciphers) { ct.ThrowIfCancellationRequested(); if (c.Name?.ToLower().Contains(query) ?? false) { - return true; + matchedCiphers.Add(c); } - if (query.Length >= 8 && c.Id.StartsWith(query)) + else if (query.Length >= 8 && c.Id.StartsWith(query)) { - return true; + lowPriorityMatchedCiphers.Add(c); } - if (c.SubTitle?.ToLower().Contains(query) ?? false) + else if (c.SubTitle?.ToLower().Contains(query) ?? false) { - return true; + lowPriorityMatchedCiphers.Add(c); } - if (c.Login?.Uri?.ToLower()?.Contains(query) ?? false) + else if (c.Login?.Uri?.ToLower()?.Contains(query) ?? false) { - return true; + lowPriorityMatchedCiphers.Add(c); } - return false; - }).ToList(); + } + + ct.ThrowIfCancellationRequested(); + matchedCiphers.AddRange(lowPriorityMatchedCiphers); + return matchedCiphers; } public async Task> SearchSendsAsync(string query, Func filter = null, @@ -133,25 +139,31 @@ namespace Bit.Core.Services public List SearchSendsBasic(List sends, string query, CancellationToken ct = default, bool deleted = false) { + var matchedSends = new List(); + var lowPriorityMatchSends = new List(); ct.ThrowIfCancellationRequested(); query = query.Trim().ToLower(); - return sends.Where(s => + + foreach (var s in sends) { ct.ThrowIfCancellationRequested(); if (s.Name?.ToLower().Contains(query) ?? false) { - return true; + matchedSends.Add(s); } - if (s.Text?.Text?.ToLower().Contains(query) ?? false) + else if (s.Text?.Text?.ToLower().Contains(query) ?? false) { - return true; + lowPriorityMatchSends.Add(s); } - if (s.File?.FileName?.ToLower()?.Contains(query) ?? false) + else if (s.File?.FileName?.ToLower()?.Contains(query) ?? false) { - return true; + lowPriorityMatchSends.Add(s); } - return false; - }).ToList(); + } + + ct.ThrowIfCancellationRequested(); + matchedSends.AddRange(lowPriorityMatchSends); + return matchedSends; } } } diff --git a/src/Core/Services/SendService.cs b/src/Core/Services/SendService.cs index d0b535df8..b59fad7e6 100644 --- a/src/Core/Services/SendService.cs +++ b/src/Core/Services/SendService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -20,9 +20,8 @@ namespace Bit.Core.Services { private List _decryptedSendsCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; - private readonly IStorageService _storageService; private readonly II18nService _i18nService; private readonly ICryptoFunctionService _cryptoFunctionService; private Task> _getAllDecryptedTask; @@ -30,27 +29,23 @@ namespace Bit.Core.Services public SendService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IApiService apiService, IFileUploadService fileUploadService, - IStorageService storageService, II18nService i18nService, ICryptoFunctionService cryptoFunctionService) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _apiService = apiService; _fileUploadService = fileUploadService; - _storageService = storageService; _i18nService = i18nService; _cryptoFunctionService = cryptoFunctionService; } - public static string GetSendKey(string userId) => string.Format("sends_{0}", userId); - public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(GetSendKey(userId)); + await _stateService.SetEncryptedSendsAsync(null, userId); ClearCache(); } @@ -58,8 +53,7 @@ namespace Bit.Core.Services public async Task DeleteAsync(params string[] ids) { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); if (sends == null) { @@ -71,7 +65,7 @@ namespace Bit.Core.Services sends.Remove(id); } - await _storageService.SaveAsync(GetSendKey(userId), sends); + await _stateService.SetEncryptedSendsAsync(sends); ClearCache(); } @@ -138,8 +132,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List(); } @@ -179,8 +172,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); if (sends == null || !sends.ContainsKey(id)) { @@ -192,8 +184,7 @@ namespace Bit.Core.Services public async Task ReplaceAsync(Dictionary sends) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(GetSendKey(userId), sends); + await _stateService.SetEncryptedSendsAsync(sends); _decryptedSendsCache = null; } @@ -209,7 +200,8 @@ namespace Bit.Core.Services response = await _apiService.PostSendAsync(request); break; case SendType.File: - try{ + try + { var uploadDataResponse = await _apiService.PostFileTypeSendAsync(request); response = uploadDataResponse.SendResponse; @@ -221,7 +213,8 @@ namespace Bit.Core.Services } catch { - if (response != default){ + if (response != default) + { await _apiService.DeleteSendAsync(response.Id); } throw; @@ -237,13 +230,14 @@ namespace Bit.Core.Services response = await _apiService.PutSendAsync(send.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await UpsertAsync(new SendData(response, userId)); return response.Id; } [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] - private async Task LegacyServerSendFileUpload(SendRequest request, Send send, EncByteArray encryptedFileData) { + private async Task LegacyServerSendFileUpload(SendRequest request, Send send, EncByteArray encryptedFileData) + { var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}") { { new StringContent(JsonConvert.SerializeObject(request)), "model" }, @@ -255,8 +249,7 @@ namespace Bit.Core.Services public async Task UpsertAsync(params SendData[] sends) { - var userId = await _userService.GetUserIdAsync(); - var knownSends = await _storageService.GetAsync>(GetSendKey(userId)) ?? + var knownSends = await _stateService.GetEncryptedSendsAsync() ?? new Dictionary(); foreach (var send in sends) @@ -264,14 +257,14 @@ namespace Bit.Core.Services knownSends[send.Id] = send; } - await _storageService.SaveAsync(GetSendKey(userId), knownSends); + await _stateService.SetEncryptedSendsAsync(knownSends); _decryptedSendsCache = null; } public async Task RemovePasswordWithServerAsync(string id) { var response = await _apiService.PutSendRemovePasswordAsync(id); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await UpsertAsync(new SendData(response, userId)); } diff --git a/src/Core/Services/SettingsService.cs b/src/Core/Services/SettingsService.cs index c0e776009..8fc72cfec 100644 --- a/src/Core/Services/SettingsService.cs +++ b/src/Core/Services/SettingsService.cs @@ -1,28 +1,22 @@ -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Newtonsoft.Json.Linq; namespace Bit.Core.Services { public class SettingsService : ISettingsService { - private const string Keys_SettingsFormat = "settings_{0}"; private const string Keys_EquivalentDomains = "equivalentDomains"; - private readonly IUserService _userService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private Dictionary _settingsCache; public SettingsService( - IUserService userService, - IStorageService storageService) + IStateService stateService) { - _userService = userService; - _storageService = storageService; + _stateService = stateService; } public void ClearCache() @@ -49,7 +43,7 @@ namespace Bit.Core.Services public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_SettingsFormat, userId)); + await _stateService.SetSettingsAsync(null, userId); ClearCache(); } @@ -59,16 +53,13 @@ namespace Bit.Core.Services { if (_settingsCache == null) { - var userId = await _userService.GetUserIdAsync(); - _settingsCache = await _storageService.GetAsync>( - string.Format(Keys_SettingsFormat, userId)); + _settingsCache = await _stateService.GetSettingsAsync(); } return _settingsCache; } private async Task SetSettingsKeyAsync(string key, T value) { - var userId = await _userService.GetUserIdAsync(); var settings = await GetSettingsAsync(); if (settings == null) { @@ -82,7 +73,7 @@ namespace Bit.Core.Services { settings.Add(key, value); } - await _storageService.SaveAsync(string.Format(Keys_SettingsFormat, userId), settings); + await _stateService.SetSettingsAsync(settings); _settingsCache = settings; } } diff --git a/src/Core/Services/StateMigrationService.cs b/src/Core/Services/StateMigrationService.cs new file mode 100644 index 000000000..5574ad72f --- /dev/null +++ b/src/Core/Services/StateMigrationService.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Services +{ + public class StateMigrationService : IStateMigrationService + { + private const int StateVersion = 3; + + private readonly IStorageService _preferencesStorageService; + private readonly IStorageService _liteDbStorageService; + private readonly IStorageService _secureStorageService; + + private enum Storage + { + LiteDb, + Prefs, + Secure, + } + + public StateMigrationService(IStorageService liteDbStorageService, IStorageService preferenceStorageService, + IStorageService secureStorageService) + { + _liteDbStorageService = liteDbStorageService; + _preferencesStorageService = preferenceStorageService; + _secureStorageService = secureStorageService; + } + + public async Task NeedsMigration() + { + var lastVersion = await GetLastStateVersionAsync(); + if (lastVersion == 0) + { + // fresh install, set current/latest version for availability going forward + lastVersion = StateVersion; + await SetLastStateVersionAsync(lastVersion); + } + return lastVersion < StateVersion; + } + + public async Task Migrate() + { + var lastVersion = await GetLastStateVersionAsync(); + switch (lastVersion) + { + case 1: + await MigrateFrom1To2Async(); + goto case 2; + case 2: + await MigrateFrom2To3Async(); + break; + } + } + + // v1 to v2 Migration + + private class V1Keys + { + internal const string EnvironmentUrlsKey = "environmentUrls"; + + } + + private async Task MigrateFrom1To2Async() + { + // move environmentUrls from LiteDB to prefs + var environmentUrls = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (environmentUrls == null) + { + throw new Exception("'environmentUrls' must be in LiteDB during migration from 1 to 2"); + } + await SetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(2); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + } + + // v2 to v3 Migration + + private class V2Keys + { + internal const string SyncOnRefreshKey = "syncOnRefresh"; + internal const string VaultTimeoutKey = "lockOption"; + internal const string VaultTimeoutActionKey = "vaultTimeoutAction"; + internal const string LastActiveTimeKey = "lastActiveTime"; + internal const string BiometricUnlockKey = "fingerprintUnlock"; + internal const string ProtectedPin = "protectedPin"; + internal const string PinProtectedKey = "pinProtectedKey"; + internal const string DefaultUriMatch = "defaultUriMatch"; + internal const string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; + internal const string EnvironmentUrlsKey = "environmentUrls"; + internal const string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; + internal const string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; + internal const string DisableFaviconKey = "disableFavicon"; + internal const string ThemeKey = "theme"; + internal const string ClearClipboardKey = "clearClipboard"; + internal const string PreviousPageKey = "previousPage"; + internal const string InlineAutofillEnabledKey = "inlineAutofillEnabled"; + internal const string InvalidUnlockAttempts = "invalidUnlockAttempts"; + internal const string PasswordRepromptAutofillKey = "passwordRepromptAutofillKey"; + internal const string PasswordVerifiedAutofillKey = "passwordVerifiedAutofillKey"; + internal const string MigratedFromV1 = "migratedFromV1"; + internal const string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; + internal const string TriedV1Resync = "triedV1Resync"; + internal const string Keys_UserId = "userId"; + internal const string Keys_UserEmail = "userEmail"; + internal const string Keys_Stamp = "securityStamp"; + internal const string Keys_Kdf = "kdf"; + internal const string Keys_KdfIterations = "kdfIterations"; + internal const string Keys_EmailVerified = "emailVerified"; + internal const string Keys_ForcePasswordReset = "forcePasswordReset"; + internal const string Keys_AccessToken = "accessToken"; + internal const string Keys_RefreshToken = "refreshToken"; + internal const string Keys_LocalData = "ciphersLocalData"; + internal const string Keys_NeverDomains = "neverDomains"; + internal const string Keys_Key = "key"; + internal const string Keys_EncOrgKeys = "encOrgKeys"; + internal const string Keys_EncPrivateKey = "encPrivateKey"; + internal const string Keys_EncKey = "encKey"; + internal const string Keys_KeyHash = "keyHash"; + internal const string Keys_UsesKeyConnector = "usesKeyConnector"; + internal const string Keys_PassGenOptions = "passwordGenerationOptions"; + internal const string Keys_PassGenHistory = "generatedPasswordHistory"; + } + + private async Task MigrateFrom2To3Async() + { + // build account and state + var userId = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + var email = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + string name = null; + var hasPremiumPersonally = false; + var accessToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + if (!string.IsNullOrWhiteSpace(accessToken)) + { + var tokenService = ServiceContainer.Resolve("tokenService"); + await tokenService.SetAccessTokenAsync(accessToken, true); + + if (string.IsNullOrWhiteSpace(userId)) + { + userId = tokenService.GetUserId(); + } + if (string.IsNullOrWhiteSpace(email)) + { + email = tokenService.GetEmail(); + } + name = tokenService.GetName(); + hasPremiumPersonally = tokenService.GetPremium(); + } + if (string.IsNullOrWhiteSpace(userId)) + { + throw new Exception("'userId' must be in LiteDB during migration from 2 to 3"); + } + + var kdfType = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + var kdfIterations = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + var stamp = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + var emailVerified = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + var refreshToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + var account = new Account( + new Account.AccountProfile() + { + UserId = userId, + Email = email, + Name = name, + Stamp = stamp, + KdfType = (KdfType?)kdfType, + KdfIterations = kdfIterations, + EmailVerified = emailVerified, + HasPremiumPersonally = hasPremiumPersonally, + }, + new Account.AccountTokens() + { + AccessToken = accessToken, + RefreshToken = refreshToken, + } + ); + var environmentUrls = await GetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + var vaultTimeout = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + var vaultTimeoutAction = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + account.Settings = new Account.AccountSettings() + { + EnvironmentUrls = environmentUrls, + VaultTimeout = vaultTimeout, + VaultTimeoutAction = + vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock, + }; + var state = new State { Accounts = new Dictionary { [userId] = account } }; + state.ActiveUserId = userId; + await SetValueAsync(Storage.LiteDb, Constants.StateKey, state); + + // migrate user-specific non-state data + var syncOnRefresh = await GetValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await SetValueAsync(Storage.LiteDb, Constants.SyncOnRefreshKey(userId), syncOnRefresh); + var lastActiveTime = await GetValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await SetValueAsync(Storage.LiteDb, Constants.LastActiveTimeKey(userId), lastActiveTime); + var biometricUnlock = await GetValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await SetValueAsync(Storage.LiteDb, Constants.BiometricUnlockKey(userId), biometricUnlock); + var protectedPin = await GetValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await SetValueAsync(Storage.LiteDb, Constants.ProtectedPinKey(userId), protectedPin); + var pinProtectedKey = await GetValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await SetValueAsync(Storage.LiteDb, Constants.PinProtectedKey(userId), pinProtectedKey); + var defaultUriMatch = await GetValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await SetValueAsync(Storage.LiteDb, Constants.DefaultUriMatchKey(userId), defaultUriMatch); + var disableAutoTotpCopy = await GetValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await SetValueAsync(Storage.LiteDb, Constants.DisableAutoTotpCopyKey(userId), disableAutoTotpCopy); + var autofillDisableSavePrompt = + await GetValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await SetValueAsync(Storage.LiteDb, Constants.AutofillDisableSavePromptKey(userId), + autofillDisableSavePrompt); + var autofillBlacklistedUris = + await GetValueAsync>(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await SetValueAsync(Storage.LiteDb, Constants.AutofillBlacklistedUrisKey(userId), autofillBlacklistedUris); + var disableFavicon = await GetValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await SetValueAsync(Storage.LiteDb, Constants.DisableFaviconKey(userId), disableFavicon); + var theme = await GetValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await SetValueAsync(Storage.LiteDb, Constants.ThemeKey(userId), theme); + var clearClipboard = await GetValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await SetValueAsync(Storage.LiteDb, Constants.ClearClipboardKey(userId), clearClipboard); + var previousPage = await GetValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await SetValueAsync(Storage.LiteDb, Constants.PreviousPageKey(userId), previousPage); + var inlineAutofillEnabled = await GetValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await SetValueAsync(Storage.LiteDb, Constants.InlineAutofillEnabledKey(userId), inlineAutofillEnabled); + var invalidUnlockAttempts = await GetValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await SetValueAsync(Storage.LiteDb, Constants.InvalidUnlockAttemptsKey(userId), invalidUnlockAttempts); + var passwordRepromptAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await SetValueAsync(Storage.LiteDb, Constants.PasswordRepromptAutofillKey(userId), + passwordRepromptAutofill); + var passwordVerifiedAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await SetValueAsync(Storage.LiteDb, Constants.PasswordVerifiedAutofillKey(userId), + passwordVerifiedAutofill); + var cipherLocalData = await GetValueAsync>>(Storage.LiteDb, + V2Keys.Keys_LocalData); + await SetValueAsync(Storage.LiteDb, Constants.LocalDataKey(userId), cipherLocalData); + var neverDomains = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await SetValueAsync(Storage.LiteDb, Constants.NeverDomainsKey(userId), neverDomains); + var key = await GetValueAsync(Storage.Secure, V2Keys.Keys_Key); + await SetValueAsync(Storage.Secure, Constants.KeyKey(userId), key); + var encOrgKeys = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await SetValueAsync(Storage.LiteDb, Constants.EncOrgKeysKey(userId), encOrgKeys); + var encPrivateKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await SetValueAsync(Storage.LiteDb, Constants.EncPrivateKeyKey(userId), encPrivateKey); + var encKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await SetValueAsync(Storage.LiteDb, Constants.EncKeyKey(userId), encKey); + var keyHash = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await SetValueAsync(Storage.LiteDb, Constants.KeyHashKey(userId), keyHash); + var usesKeyConnector = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await SetValueAsync(Storage.LiteDb, Constants.UsesKeyConnectorKey(userId), usesKeyConnector); + var passGenOptions = + await GetValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await SetValueAsync(Storage.LiteDb, Constants.PassGenOptionsKey(userId), passGenOptions); + var passGenHistory = + await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + await SetValueAsync(Storage.LiteDb, Constants.PassGenHistoryKey(userId), passGenHistory); + + // migrate global non-state data + await SetValueAsync(Storage.Prefs, Constants.PreAuthEnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(3); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_ForcePasswordReset); + await RemoveValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await RemoveValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1AutofillPromptShown); + await RemoveValueAsync(Storage.Prefs, V2Keys.TriedV1Resync); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_LocalData); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await RemoveValueAsync(Storage.Secure, V2Keys.Keys_Key); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + } + + // Helpers + + private async Task GetLastStateVersionAsync() + { + var lastVersion = await GetValueAsync(Storage.Prefs, Constants.StateVersionKey); + if (lastVersion != null) + { + return lastVersion.Value; + } + + // check for v1 state + var v1EnvUrls = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (v1EnvUrls != null) + { + // environmentUrls still in LiteDB (never migrated to prefs), this is v1 + return 1; + } + + // check for v2 state + var v2UserId = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + if (v2UserId != null) + { + // standalone userId still exists (never moved to Account object), this is v2 + return 2; + } + + // this is a fresh install + return 0; + } + + private async Task SetLastStateVersionAsync(int value) + { + await SetValueAsync(Storage.Prefs, Constants.StateVersionKey, value); + } + + private async Task GetValueAsync(Storage storage, string key) + { + var value = await GetStorageService(storage).GetAsync(key); + return value; + } + + private async Task SetValueAsync(Storage storage, string key, T value) + { + if (value == null) + { + await RemoveValueAsync(storage, key); + return; + } + await GetStorageService(storage).SaveAsync(key, value); + } + + private async Task RemoveValueAsync(Storage storage, string key) + { + await GetStorageService(storage).RemoveAsync(key); + } + + private IStorageService GetStorageService(Storage storage) + { + switch (storage) + { + case Storage.Secure: + return _secureStorageService; + case Storage.Prefs: + return _preferencesStorageService; + default: + return _liteDbStorageService; + } + } + } +} diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index a40bd6fb7..23d8de670 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1,44 +1,1554 @@ -using Bit.Core.Abstractions; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; namespace Bit.Core.Services { public class StateService : IStateService { - private readonly Dictionary _state = new Dictionary(); + private readonly IStorageService _storageService; + private readonly IStorageService _secureStorageService; - public Task GetAsync(string key) + private State _state; + private bool _migrationChecked; + + public List AccountViews { get; set; } + + public StateService(IStorageService storageService, IStorageService secureStorageService) { - return Task.FromResult(_state.ContainsKey(key) ? (T)_state[key] : (T)(object)null); + _storageService = storageService; + _secureStorageService = secureStorageService; } - public Task SaveAsync(string key, T obj) + public async Task GetActiveUserIdAsync() { - if (_state.ContainsKey(key)) + await CheckStateAsync(); + + var activeUserId = _state?.ActiveUserId; + if (activeUserId == null) { - _state[key] = obj; + var state = await GetStateFromStorageAsync(); + activeUserId = state?.ActiveUserId; + } + return activeUserId; + } + + public async Task IsActiveAccountAsync(string userId = null) + { + if (userId == null) + { + return true; + } + return userId == await GetActiveUserIdAsync(); + } + + public async Task SetActiveUserAsync(string userId) + { + if (userId != null) + { + await ValidateUserAsync(userId); + } + await CheckStateAsync(); + var state = await GetStateFromStorageAsync(); + state.ActiveUserId = userId; + await SaveStateToStorageAsync(state); + _state.ActiveUserId = userId; + + // Update pre-auth settings based on now-active user + await SetRememberedEmailAsync(await GetEmailAsync()); + await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync()); + await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync()); + } + + public async Task IsAuthenticatedAsync(string userId = null) + { + return await GetAccessTokenAsync(userId) != null; + } + + public async Task GetUserIdAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentNullException(nameof(email)); + } + + await CheckStateAsync(); + if (_state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var accountEmail = account.Value?.Profile?.Email; + if (accountEmail == email) + { + return account.Value.Profile.UserId; + } + } + } + return null; + } + + public async Task RefreshAccountViewsAsync(bool allowAddAccountRow) + { + await CheckStateAsync(); + + if (AccountViews == null) + { + AccountViews = new List(); } else { - _state.Add(key, obj); + AccountViews.Clear(); } - return Task.FromResult(0); - } - public Task RemoveAsync(string key) - { - if (_state.ContainsKey(key)) + var accountList = _state?.Accounts?.Values.ToList(); + if (accountList == null) { - _state.Remove(key); + return; + } + var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + foreach (var account in accountList) + { + var isActiveAccount = account.Profile.UserId == _state.ActiveUserId; + var accountView = new AccountView(account, isActiveAccount); + + if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(accountView.UserId) || + await vaultTimeoutService.ShouldLogOutByTimeoutAsync(accountView.UserId)) + { + accountView.AuthStatus = AuthenticationStatus.LoggedOut; + } + else if (await vaultTimeoutService.IsLockedAsync(accountView.UserId) || + await vaultTimeoutService.ShouldLockAsync(accountView.UserId)) + { + accountView.AuthStatus = AuthenticationStatus.Locked; + } + else + { + accountView.AuthStatus = AuthenticationStatus.Unlocked; + } + AccountViews.Add(accountView); + } + if (allowAddAccountRow && AccountViews.Count < Constants.MaxAccounts) + { + AccountViews.Add(new AccountView()); } - return Task.FromResult(0); } - public Task PurgeAsync() + public async Task AddAccountAsync(Account account) { - _state.Clear(); - return Task.FromResult(0); + await ScaffoldNewAccountAsync(account); + await SetActiveUserAsync(account.Profile.UserId); + await RefreshAccountViewsAsync(true); + } + + public async Task LogoutAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + await CheckStateAsync(); + await RemoveAccountAsync(userId, userInitiated); + + // If user initiated logout (not vault timeout) and ActiveUserId is null after account removal, find the + // next user to make active, if any + if (userInitiated && _state?.ActiveUserId == null && _state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var uid = account.Value?.Profile?.UserId; + if (uid == null) + { + continue; + } + await SetActiveUserAsync(uid); + break; + } + } + } + + public async Task GetPreAuthEnvironmentUrlsAsync() + { + return await GetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value) + { + await SetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetEnvironmentUrlsAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.EnvironmentUrls; + } + + public async Task GetBiometricUnlockAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.BiometricUnlockKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetBiometricUnlockAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.BiometricUnlockKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetBiometricLockedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.BiometricLocked ?? true; + } + + public async Task SetBiometricLockedAsync(bool value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.BiometricLocked = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task CanAccessPremiumAsync(string userId = null) + { + if (userId == null) + { + userId = await GetActiveUserIdAsync(); + } + if (!await IsAuthenticatedAsync(userId)) + { + return false; + } + + var account = await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())); + if (account?.Profile?.HasPremiumPersonally.GetValueOrDefault() ?? false) + { + return true; + } + + var organizationService = ServiceContainer.Resolve("organizationService"); + var organizations = await organizationService.GetAllAsync(userId); + return organizations?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; + } + + public async Task GetProtectedPinAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ProtectedPinKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetProtectedPinAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ProtectedPinKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPinProtectedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PinProtectedKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPinProtectedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PinProtectedKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPinProtectedKeyAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.PinProtectedKey; + } + + public async Task SetPinProtectedKeyAsync(EncString value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.PinProtectedKey = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKdfTypeAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.KdfType; + } + + public async Task SetKdfTypeAsync(KdfType? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.KdfType = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKdfIterationsAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.KdfIterations; + } + + public async Task SetKdfIterationsAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.KdfIterations = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + var key = Constants.KeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + var key = Constants.KeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetKeyDecryptedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.VolatileData?.Key; + } + + public async Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.VolatileData.Key = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyHashAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.KeyHashKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetKeyHashAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.KeyHashKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetEncKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncKeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetEncKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncKeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetOrgKeysEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncOrgKeysKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncOrgKeysKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPrivateKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncPrivateKeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPrivateKeyEncryptedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncPrivateKeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetAutofillBlacklistedUrisAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetAutofillBlacklistedUrisAsync(List value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAutofillTileAddedAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.AutofillTileAdded; + return await GetValueAsync(key, options); + } + + public async Task SetAutofillTileAddedAsync(bool? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.AutofillTileAdded; + await SetValueAsync(key, value, options); + } + + public async Task GetEmailAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Email; + } + + public async Task GetNameAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Name; + } + + public async Task SetNameAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.Name = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetOrgIdentifierAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.OrgIdentifier; + } + + public async Task GetLastActiveTimeAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastActiveTimeKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetLastActiveTimeAsync(long? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastActiveTimeKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetVaultTimeoutAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.VaultTimeout; + } + + public async Task SetVaultTimeoutAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Settings.VaultTimeout = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetVaultTimeoutActionAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.VaultTimeoutAction; + } + + public async Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Settings.VaultTimeoutAction = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetLastFileCacheClearAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastFileCacheClearKey; + return await GetValueAsync(key, options); + } + + public async Task SetLastFileCacheClearAsync(DateTime? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastFileCacheClearKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPreviousPageInfoAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PreviousPageKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PreviousPageKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetInvalidUnlockAttemptsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetLastBuildAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastBuildKey; + return await GetValueAsync(key, options); + } + + public async Task SetLastBuildAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastBuildKey; + await SetValueAsync(key, value, options); + } + + public async Task GetDisableFaviconAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableFaviconKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDisableFaviconAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableFaviconKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + + // TODO remove this to restore per-account DisableFavicon support + SetValueGloballyAsync(Constants.DisableFaviconKey, value, reconciledOptions).FireAndForget(); + } + + public async Task GetDisableAutoTotpCopyAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetInlineAutofillEnabledAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InlineAutofillEnabledKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetInlineAutofillEnabledAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InlineAutofillEnabledKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAutofillDisableSavePromptAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task>> GetLocalDataAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LocalDataKey(reconciledOptions.UserId); + return await GetValueAsync>>(key, reconciledOptions); + } + + public async Task SetLocalDataAsync(Dictionary> value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LocalDataKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedCiphersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CiphersKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedCiphersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CiphersKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetDefaultUriMatchAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DefaultUriMatchKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDefaultUriMatchAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DefaultUriMatchKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetNeverDomainsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.NeverDomainsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetNeverDomainsAsync(HashSet value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.NeverDomainsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetClearClipboardAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ClearClipboardKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetClearClipboardAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ClearClipboardKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedCollectionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CollectionsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CollectionsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordRepromptAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordVerifiedAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetLastSyncAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastSyncKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetLastSyncAsync(DateTime? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastSyncKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetSecurityStampAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Stamp; + } + + public async Task SetSecurityStampAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.Stamp = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetEmailVerifiedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.EmailVerified ?? false; + } + + public async Task SetEmailVerifiedAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.EmailVerified = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetSyncOnRefreshAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SyncOnRefreshKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetSyncOnRefreshAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SyncOnRefreshKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetRememberedEmailAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedEmailKey; + return await GetValueAsync(key, options); + } + + public async Task SetRememberedEmailAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedEmailKey; + await SetValueAsync(key, value, options); + } + + public async Task GetRememberedOrgIdentifierAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedOrgIdentifierKey; + return await GetValueAsync(key, options); + } + + public async Task SetRememberedOrgIdentifierAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedOrgIdentifierKey; + await SetValueAsync(key, value, options); + } + + public async Task GetThemeAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ThemeKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetThemeAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ThemeKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + + // TODO remove this to restore per-account Theme support + SetValueGloballyAsync(Constants.ThemeKey, value, reconciledOptions).FireAndForget(); + } + + public async Task GetAddSitePromptShownAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AddSitePromptShownKey; + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetAddSitePromptShownAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AddSitePromptShownKey; + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPushInitialPromptShownAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInitialPromptShownKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushInitialPromptShownAsync(bool? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInitialPromptShownKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushLastRegistrationDateAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushLastRegistrationDateKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushLastRegistrationDateAsync(DateTime? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushLastRegistrationDateKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushInstallationRegistrationErrorAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInstallationRegistrationErrorKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushInstallationRegistrationErrorAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInstallationRegistrationErrorKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushCurrentTokenAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushCurrentTokenKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushCurrentTokenAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushCurrentTokenKey; + await SetValueAsync(key, value, options); + } + + public async Task> GetEventCollectionAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.EventCollectionKey; + return await GetValueAsync>(key, options); + } + + public async Task SetEventCollectionAsync(List value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.EventCollectionKey; + await SetValueAsync(key, value, options); + } + + public async Task> GetEncryptedFoldersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.FoldersKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedFoldersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.FoldersKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedPoliciesAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PoliciesKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedPoliciesAsync(Dictionary value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PoliciesKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPushRegisteredTokenAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushRegisteredTokenKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushRegisteredTokenAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushRegisteredTokenKey; + await SetValueAsync(key, value, options); + } + + public async Task GetUsesKeyConnectorAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.UsesKeyConnectorKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetUsesKeyConnectorAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.UsesKeyConnectorKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetOrganizationsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.OrganizationsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetOrganizationsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.OrganizationsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordGenerationOptionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenOptionsKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenOptionsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedPasswordGenerationHistory(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenHistoryKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedPasswordGenerationHistoryAsync(List value, + string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenHistoryKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedSendsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SendsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedSendsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SendsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetSettingsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SettingsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetSettingsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SettingsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAccessTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.AccessToken; + } + + public async Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.AccessToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetRefreshTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.RefreshToken; + } + + public async Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.RefreshToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetTwoFactorTokenAsync(string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + var key = Constants.TwoFactorTokenKey(reconciledOptions.Email); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetTwoFactorTokenAsync(string value, string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + var key = Constants.TwoFactorTokenKey(reconciledOptions.Email); + await SetValueAsync(key, value, reconciledOptions); + } + + // Helpers + + private async Task GetValueAsync(string key, StorageOptions options) + { + return await GetStorageService(options).GetAsync(key); + } + + private async Task SetValueAsync(string key, T value, StorageOptions options) + { + if (value == null) + { + await GetStorageService(options).RemoveAsync(key); + return; + } + await GetStorageService(options).SaveAsync(key, value); + } + + private async Task SetValueGloballyAsync(Func keyPrefix, T value, StorageOptions options) + { + if (value == null) + { + // don't remove values globally + return; + } + await CheckStateAsync(); + if (_state?.Accounts == null) + { + return; + } + // userId from options was already applied, skip those + var userIdToSkip = options.UserId; + foreach (var account in _state.Accounts) + { + var uid = account.Value?.Profile?.UserId; + if (uid != null && uid != userIdToSkip) + { + await SetValueAsync(keyPrefix(uid), value, options); + } + } + } + + private IStorageService GetStorageService(StorageOptions options) + { + return options.UseSecureStorage.GetValueOrDefault(false) ? _secureStorageService : _storageService; + } + + private async Task GetAccountAsync(StorageOptions options) + { + await CheckStateAsync(); + + if (options?.UserId == null) + { + return null; + } + + // Memory + if (_state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + if (_state.Accounts[options.UserId].VolatileData == null) + { + _state.Accounts[options.UserId].VolatileData = new Account.AccountVolatileData(); + } + return _state.Accounts[options.UserId]; + } + + // Storage + var state = await GetStateFromStorageAsync(); + if (state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + state.Accounts[options.UserId].VolatileData = new Account.AccountVolatileData(); + return state.Accounts[options.UserId]; + } + + return null; + } + + private async Task SaveAccountAsync(Account account, StorageOptions options = null) + { + if (account?.Profile?.UserId == null) + { + throw new Exception("account?.Profile?.UserId cannot be null"); + } + + await CheckStateAsync(); + + // Memory + if (UseMemory(options)) + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + + // Storage + if (UseDisk(options)) + { + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + + // Use Account copy constructor to clone with keys excluded (for storage) + state.Accounts[account.Profile.UserId] = new Account(account); + + // If we have a vault timeout and the action is log out, don't store token + if (options?.SkipTokenStorage.GetValueOrDefault() ?? false) + { + state.Accounts[account.Profile.UserId].Tokens.AccessToken = null; + state.Accounts[account.Profile.UserId].Tokens.RefreshToken = null; + } + + await SaveStateToStorageAsync(state); + } + } + + private async Task RemoveAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + var email = await GetEmailAsync(userId); + + // Memory + if (_state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + _state.Accounts.Remove(userId); + } + else + { + _state.Accounts[userId].Tokens.AccessToken = null; + _state.Accounts[userId].Tokens.RefreshToken = null; + _state.Accounts[userId].VolatileData = null; + } + } + if (userInitiated && _state?.ActiveUserId == userId) + { + _state.ActiveUserId = null; + } + + // Storage + var stateModified = false; + var state = await GetStateFromStorageAsync(); + if (state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + state.Accounts.Remove(userId); + } + else + { + state.Accounts[userId].Tokens.AccessToken = null; + state.Accounts[userId].Tokens.RefreshToken = null; + } + stateModified = true; + } + if (userInitiated && state?.ActiveUserId == userId) + { + state.ActiveUserId = null; + stateModified = true; + } + if (stateModified) + { + await SaveStateToStorageAsync(state); + } + + // Non-state storage + await SetBiometricUnlockAsync(null, userId); + await SetProtectedPinAsync(null, userId); + await SetPinProtectedAsync(null, userId); + await SetKeyEncryptedAsync(null, userId); + await SetKeyHashAsync(null, userId); + await SetEncKeyEncryptedAsync(null, userId); + await SetOrgKeysEncryptedAsync(null, userId); + await SetPrivateKeyEncryptedAsync(null, userId); + await SetLastActiveTimeAsync(null, userId); + await SetPreviousPageInfoAsync(null, userId); + await SetInvalidUnlockAttemptsAsync(null, userId); + await SetLocalDataAsync(null, userId); + await SetEncryptedCiphersAsync(null, userId); + await SetEncryptedCollectionsAsync(null, userId); + await SetLastSyncAsync(null, userId); + await SetEncryptedFoldersAsync(null, userId); + await SetEncryptedPoliciesAsync(null, userId); + await SetUsesKeyConnectorAsync(null, userId); + await SetOrganizationsAsync(null, userId); + await SetEncryptedPasswordGenerationHistoryAsync(null, userId); + await SetEncryptedSendsAsync(null, userId); + await SetSettingsAsync(null, userId); + + if (userInitiated) + { + // user initiated logout (not vault timeout or scaffolding new account) so remove remaining settings + await SetAutofillBlacklistedUrisAsync(null, userId); + await SetDisableFaviconAsync(null, userId); + await SetDisableAutoTotpCopyAsync(null, userId); + await SetInlineAutofillEnabledAsync(null, userId); + await SetAutofillDisableSavePromptAsync(null, userId); + await SetDefaultUriMatchAsync(null, userId); + await SetNeverDomainsAsync(null, userId); + await SetClearClipboardAsync(null, userId); + await SetPasswordRepromptAutofillAsync(null, userId); + await SetPasswordVerifiedAutofillAsync(null, userId); + await SetSyncOnRefreshAsync(null, userId); + await SetThemeAsync(null, userId); + await SetAddSitePromptShownAsync(null, userId); + await SetPasswordGenerationOptionsAsync(null, userId); + } + } + + private async Task ScaffoldNewAccountAsync(Account account) + { + await CheckStateAsync(); + var currentTheme = await GetThemeAsync(); + var currentDisableFavicons = await GetDisableFaviconAsync(); + + account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync(); + + // Storage + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + if (state.Accounts.ContainsKey(account.Profile.UserId)) + { + // Run cleanup pass on existing account before proceeding + await RemoveAccountAsync(account.Profile.UserId, false); + var existingAccount = state.Accounts[account.Profile.UserId]; + account.Settings.VaultTimeout = existingAccount.Settings.VaultTimeout; + account.Settings.VaultTimeoutAction = existingAccount.Settings.VaultTimeoutAction; + } + + // New account defaults + if (account.Settings.VaultTimeout == null) + { + account.Settings.VaultTimeout = 15; + } + if (account.Settings.VaultTimeoutAction == null) + { + account.Settings.VaultTimeoutAction = VaultTimeoutAction.Lock; + } + await SetThemeAsync(currentTheme, account.Profile.UserId); + await SetDisableFaviconAsync(currentDisableFavicons, account.Profile.UserId); + + state.Accounts[account.Profile.UserId] = account; + await SaveStateToStorageAsync(state); + + // Memory + if (_state == null) + { + _state = state; + } + else + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + } + + private StorageOptions ReconcileOptions(StorageOptions requestedOptions, StorageOptions defaultOptions) + { + if (requestedOptions == null) + { + return defaultOptions; + } + requestedOptions.StorageLocation = requestedOptions.StorageLocation ?? defaultOptions.StorageLocation; + requestedOptions.UseSecureStorage = requestedOptions.UseSecureStorage ?? defaultOptions.UseSecureStorage; + requestedOptions.UserId = requestedOptions.UserId ?? defaultOptions.UserId; + requestedOptions.Email = requestedOptions.Email ?? defaultOptions.Email; + requestedOptions.SkipTokenStorage = requestedOptions.SkipTokenStorage ?? defaultOptions.SkipTokenStorage; + return requestedOptions; + } + + private async Task GetDefaultStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Both, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultSecureStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Disk, + UseSecureStorage = true, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultInMemoryOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Memory, + UserId = await GetActiveUserIdAsync(), + }; + } + + private bool UseMemory(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Memory || + options?.StorageLocation == StorageLocation.Both; + } + + private bool UseDisk(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Disk || + options?.StorageLocation == StorageLocation.Both; + } + + private async Task GetStateFromStorageAsync() + { + return await _storageService.GetAsync(Constants.StateKey); + } + + private async Task SaveStateToStorageAsync(State state) + { + await _storageService.SaveAsync(Constants.StateKey, state); + } + + private async Task CheckStateAsync() + { + if (!_migrationChecked) + { + var migrationService = ServiceContainer.Resolve("stateMigrationService"); + if (await migrationService.NeedsMigration()) + { + await migrationService.Migrate(); + } + _migrationChecked = true; + } + + if (_state == null) + { + _state = await GetStateFromStorageAsync() ?? new State(); + } + } + + private async Task ValidateUserAsync(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + await CheckStateAsync(); + var accounts = _state?.Accounts; + if (accounts == null || !accounts.Any()) + { + throw new Exception("At least one account required to validate user"); + } + foreach (var account in accounts) + { + if (account.Key == userId) + { + // found match, user is valid + return; + } + } + throw new Exception("User does not exist in account list"); } } } diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index 7d7795a4e..ba90d1da9 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -1,56 +1,54 @@ -using Bit.Core.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Bit.Core.Services { public class SyncService : ISyncService { - private const string Keys_LastSyncFormat = "lastSync_{0}"; - - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; private readonly ISettingsService _settingsService; private readonly IFolderService _folderService; private readonly ICipherService _cipherService; private readonly ICryptoService _cryptoService; private readonly ICollectionService _collectionService; - private readonly IStorageService _storageService; + private readonly IOrganizationService _organizationService; private readonly IMessagingService _messagingService; private readonly IPolicyService _policyService; private readonly ISendService _sendService; private readonly IKeyConnectorService _keyConnectorService; - private readonly Func _logoutCallbackAsync; + private readonly Func, Task> _logoutCallbackAsync; public SyncService( - IUserService userService, + IStateService stateService, IApiService apiService, ISettingsService settingsService, IFolderService folderService, ICipherService cipherService, ICryptoService cryptoService, ICollectionService collectionService, - IStorageService storageService, + IOrganizationService organizationService, IMessagingService messagingService, IPolicyService policyService, ISendService sendService, IKeyConnectorService keyConnectorService, - Func logoutCallbackAsync) + Func, Task> logoutCallbackAsync) { - _userService = userService; + _stateService = stateService; _apiService = apiService; _settingsService = settingsService; _folderService = folderService; _cipherService = cipherService; _cryptoService = cryptoService; _collectionService = collectionService; - _storageService = storageService; + _organizationService = organizationService; _messagingService = messagingService; _policyService = policyService; _sendService = sendService; @@ -62,28 +60,26 @@ namespace Bit.Core.Services public async Task GetLastSyncAsync() { - var userId = await _userService.GetUserIdAsync(); - if (userId == null) + if (await _stateService.GetActiveUserIdAsync() == null) { return null; } - return await _storageService.GetAsync(string.Format(Keys_LastSyncFormat, userId)); + return await _stateService.GetLastSyncAsync(); } public async Task SetLastSyncAsync(DateTime date) { - var userId = await _userService.GetUserIdAsync(); - if (userId == null) + if (await _stateService.GetActiveUserIdAsync() == null) { return; } - await _storageService.SaveAsync(string.Format(Keys_LastSyncFormat, userId), date); + await _stateService.SetLastSyncAsync(date); } public async Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false) { SyncStarted(); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); if (!isAuthenticated) { return SyncCompleted(false); @@ -101,7 +97,7 @@ namespace Bit.Core.Services await SetLastSyncAsync(now); return SyncCompleted(false); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); try { var response = await _apiService.GetSyncAsync(); @@ -131,7 +127,7 @@ namespace Bit.Core.Services public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { try { @@ -142,7 +138,7 @@ namespace Bit.Core.Services var remoteFolder = await _apiService.GetFolderAsync(notification.Id); if (remoteFolder != null) { - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); _messagingService.Send("syncedUpsertedFolder", new Dictionary { @@ -160,7 +156,7 @@ namespace Bit.Core.Services public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { await _folderService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedFolder", new Dictionary @@ -175,7 +171,7 @@ namespace Bit.Core.Services public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { try { @@ -230,7 +226,7 @@ namespace Bit.Core.Services var remoteCipher = await _apiService.GetCipherAsync(notification.Id); if (remoteCipher != null) { - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); _messagingService.Send("syncedUpsertedCipher", new Dictionary { @@ -259,7 +255,7 @@ namespace Bit.Core.Services public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { await _cipherService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedCipher", new Dictionary @@ -315,23 +311,23 @@ namespace Bit.Core.Services private async Task SyncProfileAsync(ProfileResponse response) { - var stamp = await _userService.GetSecurityStampAsync(); + var stamp = await _stateService.GetSecurityStampAsync(); if (stamp != null && stamp != response.SecurityStamp) { if (_logoutCallbackAsync != null) { - await _logoutCallbackAsync(true); + await _logoutCallbackAsync(new Tuple(response.Id, false, true)); } return; } await _cryptoService.SetEncKeyAsync(response.Key); await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); await _cryptoService.SetOrgKeysAsync(response.Organizations); - await _userService.SetSecurityStampAsync(response.SecurityStamp); + await _stateService.SetSecurityStampAsync(response.SecurityStamp); var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); - await _userService.ReplaceOrganizationsAsync(organizations); - await _userService.SetEmailVerifiedAsync(response.EmailVerified); - await _userService.SetForcePasswordReset(response.ForcePasswordReset); + await _organizationService.ReplaceAsync(organizations); + await _stateService.SetEmailVerifiedAsync(response.EmailVerified); + await _stateService.SetNameAsync(response.Name); await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector); } @@ -382,7 +378,7 @@ namespace Bit.Core.Services private async Task SyncSendsAsync(string userId, List response) { - var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ?? + var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ?? new Dictionary(); await _sendService.ReplaceAsync(sends); } diff --git a/src/Core/Services/TokenService.cs b/src/Core/Services/TokenService.cs index 09f1c44cc..9edd32b22 100644 --- a/src/Core/Services/TokenService.cs +++ b/src/Core/Services/TokenService.cs @@ -1,136 +1,108 @@ -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using Newtonsoft.Json.Linq; -using System; +using System; +using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Linq; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Newtonsoft.Json.Linq; namespace Bit.Core.Services { public class TokenService : ITokenService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; - private string _token; - private JObject _decodedToken; - private string _refreshToken; + private string _accessTokenForDecoding; + private JObject _decodedAccessToken; - private const string Keys_AccessToken = "accessToken"; - private const string Keys_RefreshToken = "refreshToken"; - private const string Keys_TwoFactorTokenFormat = "twoFactorToken_{0}"; - - public TokenService(IStorageService storageService) + public TokenService(IStateService stateService) { - _storageService = storageService; + _stateService = stateService; } public async Task SetTokensAsync(string accessToken, string refreshToken) { await Task.WhenAll( - SetTokenAsync(accessToken), + SetAccessTokenAsync(accessToken), SetRefreshTokenAsync(refreshToken)); } - public async Task SetTokenAsync(string token) + public async Task SetAccessTokenAsync(string accessToken, bool forDecodeOnly = false) { - _token = token; - _decodedToken = null; - - if (await SkipTokenStorage()) + _accessTokenForDecoding = accessToken; + _decodedAccessToken = null; + if (!forDecodeOnly) { - // If we have a vault timeout and the action is log out, don't store token - return; + await _stateService.SetAccessTokenAsync(accessToken, await SkipTokenStorage()); } - - await _storageService.SaveAsync(Keys_AccessToken, token); } public async Task GetTokenAsync() { - if (_token != null) - { - return _token; - } - _token = await _storageService.GetAsync(Keys_AccessToken); - return _token; + _accessTokenForDecoding = await _stateService.GetAccessTokenAsync(); + return _accessTokenForDecoding; } public async Task SetRefreshTokenAsync(string refreshToken) { - _refreshToken = refreshToken; - - if (await SkipTokenStorage()) - { - // If we have a vault timeout and the action is log out, don't store token - return; - } - - await _storageService.SaveAsync(Keys_RefreshToken, refreshToken); + await _stateService.SetRefreshTokenAsync(refreshToken, await SkipTokenStorage()); } public async Task GetRefreshTokenAsync() { - if (_refreshToken != null) - { - return _refreshToken; - } - _refreshToken = await _storageService.GetAsync(Keys_RefreshToken); - return _refreshToken; + return await _stateService.GetRefreshTokenAsync(); } public async Task ToggleTokensAsync() { + // load and re-save tokens to reflect latest value of SkipTokenStorage() var token = await GetTokenAsync(); var refreshToken = await GetRefreshTokenAsync(); - if (await SkipTokenStorage()) - { - await ClearTokenAsync(); - _token = token; - _refreshToken = refreshToken; - return; - } - - await SetTokenAsync(token); + await SetAccessTokenAsync(token); await SetRefreshTokenAsync(refreshToken); } public async Task SetTwoFactorTokenAsync(string token, string email) { - await _storageService.SaveAsync(string.Format(Keys_TwoFactorTokenFormat, email), token); + await _stateService.SetTwoFactorTokenAsync(token, email); } public async Task GetTwoFactorTokenAsync(string email) { - return await _storageService.GetAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + return await _stateService.GetTwoFactorTokenAsync(email); } public async Task ClearTwoFactorTokenAsync(string email) { - await _storageService.RemoveAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + await _stateService.SetTwoFactorTokenAsync(null, email); } - public async Task ClearTokenAsync() + public async Task ClearTokenAsync(string userId = null) { - _token = null; - _decodedToken = null; - _refreshToken = null; + ClearCache(); await Task.WhenAll( - _storageService.RemoveAsync(Keys_AccessToken), - _storageService.RemoveAsync(Keys_RefreshToken)); + _stateService.SetAccessTokenAsync(null, false, userId), + _stateService.SetRefreshTokenAsync(null, false, userId)); + } + + public void ClearCache() + { + _accessTokenForDecoding = null; + _decodedAccessToken = null; } public JObject DecodeToken() { - if (_decodedToken != null) + if (_decodedAccessToken != null) { - return _decodedToken; + return _decodedAccessToken; } - if (_token == null) + if (_accessTokenForDecoding == null) { - throw new InvalidOperationException("Token not found."); + throw new InvalidOperationException("Access token not found."); } - var parts = _token.Split('.'); + var parts = _accessTokenForDecoding.Split('.'); if (parts.Length != 3) { throw new InvalidOperationException("JWT must have 3 parts."); @@ -140,8 +112,8 @@ namespace Bit.Core.Services { throw new InvalidOperationException("Cannot decode the token."); } - _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); - return _decodedToken; + _decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); + return _decodedAccessToken; } public DateTime? GetTokenExpirationDate() @@ -231,8 +203,16 @@ namespace Bit.Core.Services return decoded["iss"].Value(); } - public bool GetIsExternal() + public async Task GetIsExternal() { + if (_accessTokenForDecoding == null) + { + await GetTokenAsync(); + if (_accessTokenForDecoding == null) + { + return false; + } + } var decoded = DecodeToken(); if (decoded?["amr"] == null) { @@ -243,9 +223,9 @@ namespace Bit.Core.Services private async Task SkipTokenStorage() { - var timeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - return timeout.HasValue && action == "logOut"; + var timeout = await _stateService.GetVaultTimeoutAsync(); + var action = await _stateService.GetVaultTimeoutActionAsync(); + return timeout.HasValue && action == VaultTimeoutAction.Logout; } } } diff --git a/src/Core/Services/TotpService.cs b/src/Core/Services/TotpService.cs index 0cf4dce15..df74920d7 100644 --- a/src/Core/Services/TotpService.cs +++ b/src/Core/Services/TotpService.cs @@ -1,8 +1,8 @@ -using Bit.Core.Abstractions; +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; -using System; -using System.Threading.Tasks; namespace Bit.Core.Services { @@ -10,14 +10,14 @@ namespace Bit.Core.Services { private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; public TotpService( - IStorageService storageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService) { - _storageService = storageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; } @@ -135,7 +135,7 @@ namespace Bit.Core.Services public async Task IsAutoCopyEnabledAsync() { - var disabled = await _storageService.GetAsync(Constants.DisableAutoTotpCopyKey); + var disabled = await _stateService.GetDisableAutoTotpCopyAsync(); return !disabled.GetValueOrDefault(); } } diff --git a/src/Core/Services/UserService.cs b/src/Core/Services/UserService.cs deleted file mode 100644 index c71e52e07..000000000 --- a/src/Core/Services/UserService.cs +++ /dev/null @@ -1,221 +0,0 @@ -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Bit.Core.Services -{ - public class UserService : IUserService - { - private string _userId; - private string _email; - private string _stamp; - private KdfType? _kdf; - private int? _kdfIterations; - private bool? _emailVerified; - private bool? _forcePasswordReset; - - private const string Keys_UserId = "userId"; - private const string Keys_UserEmail = "userEmail"; - private const string Keys_Stamp = "securityStamp"; - private const string Keys_Kdf = "kdf"; - private const string Keys_KdfIterations = "kdfIterations"; - private const string Keys_OrganizationsFormat = "organizations_{0}"; - private const string Keys_EmailVerified = "emailVerified"; - private const string Keys_ForcePasswordReset = "forcePasswordReset"; - - private readonly IStorageService _storageService; - private readonly ITokenService _tokenService; - - public UserService(IStorageService storageService, ITokenService tokenService) - { - _storageService = storageService; - _tokenService = tokenService; - } - - public async Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations) - { - _email = email; - _userId = userId; - _kdf = kdf; - _kdfIterations = kdfIterations; - await Task.WhenAll( - _storageService.SaveAsync(Keys_UserEmail, email), - _storageService.SaveAsync(Keys_UserId, userId), - _storageService.SaveAsync(Keys_Kdf, (int)kdf), - _storageService.SaveAsync(Keys_KdfIterations, kdfIterations)); - } - - public async Task SetSecurityStampAsync(string stamp) - { - _stamp = stamp; - await _storageService.SaveAsync(Keys_Stamp, stamp); - } - - public async Task SetEmailVerifiedAsync(bool emailVerified) - { - _emailVerified = emailVerified; - await _storageService.SaveAsync(Keys_EmailVerified, emailVerified); - } - - public async Task SetForcePasswordReset(bool forcePasswordReset) - { - _forcePasswordReset = forcePasswordReset; - await _storageService.SaveAsync(Keys_ForcePasswordReset, forcePasswordReset); - } - - public async Task GetUserIdAsync() - { - if (_userId == null) - { - _userId = await _storageService.GetAsync(Keys_UserId); - } - return _userId; - } - - public async Task GetEmailAsync() - { - if (_email == null) - { - _email = await _storageService.GetAsync(Keys_UserEmail); - } - return _email; - } - - public async Task GetSecurityStampAsync() - { - if (_stamp == null) - { - _stamp = await _storageService.GetAsync(Keys_Stamp); - } - return _stamp; - } - - public async Task GetEmailVerifiedAsync() - { - if (_emailVerified == null) - { - _emailVerified = await _storageService.GetAsync(Keys_EmailVerified); - } - return _emailVerified.GetValueOrDefault(); - } - - public async Task GetKdfAsync() - { - if (_kdf == null) - { - _kdf = (KdfType?)(await _storageService.GetAsync(Keys_Kdf)); - } - return _kdf; - } - - public async Task GetKdfIterationsAsync() - { - if (_kdfIterations == null) - { - _kdfIterations = await _storageService.GetAsync(Keys_KdfIterations); - } - return _kdfIterations; - } - - public async Task GetForcePasswordReset() - { - if (_forcePasswordReset == null) - { - _forcePasswordReset = await _storageService.GetAsync(Keys_ForcePasswordReset); - } - return _forcePasswordReset.GetValueOrDefault(); - } - - public async Task ClearAsync() - { - var userId = await GetUserIdAsync(); - await Task.WhenAll( - _storageService.RemoveAsync(Keys_UserId), - _storageService.RemoveAsync(Keys_UserEmail), - _storageService.RemoveAsync(Keys_Stamp), - _storageService.RemoveAsync(Keys_Kdf), - _storageService.RemoveAsync(Keys_KdfIterations), - _storageService.RemoveAsync(Keys_ForcePasswordReset), - ClearOrganizationsAsync(userId)); - _userId = _email = _stamp = null; - _kdf = null; - _kdfIterations = null; - } - - public async Task IsAuthenticatedAsync() - { - var token = await _tokenService.GetTokenAsync(); - if (token == null) - { - return false; - } - var userId = await GetUserIdAsync(); - return userId != null; - } - - public async Task CanAccessPremiumAsync() - { - var authed = await IsAuthenticatedAsync(); - if (!authed) - { - return false; - } - - var tokenPremium = _tokenService.GetPremium(); - if (tokenPremium) - { - return true; - } - var orgs = await GetAllOrganizationAsync(); - return orgs?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; - } - - public async Task GetOrganizationAsync(string id) - { - var userId = await GetUserIdAsync(); - var organizations = await _storageService.GetAsync>( - string.Format(Keys_OrganizationsFormat, userId)); - if (organizations == null || !organizations.ContainsKey(id)) - { - return null; - } - return new Organization(organizations[id]); - } - - public async Task GetOrganizationByIdentifierAsync(string identifier) - { - var userId = await GetUserIdAsync(); - var organizations = await GetAllOrganizationAsync(); - - if (organizations == null || organizations.Count == 0) - { - return null; - } - - return organizations.FirstOrDefault(o => o.Identifier == identifier); - } - - public async Task> GetAllOrganizationAsync() - { - var userId = await GetUserIdAsync(); - var organizations = await _storageService.GetAsync>( - string.Format(Keys_OrganizationsFormat, userId)); - return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); - } - - public async Task ReplaceOrganizationsAsync(Dictionary organizations) - { - var userId = await GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_OrganizationsFormat, userId), organizations); - } - - public async Task ClearOrganizationsAsync(string userId) - { - await _storageService.RemoveAsync(string.Format(Keys_OrganizationsFormat, userId)); - } - } -} diff --git a/src/Core/Services/VaultTimeoutService.cs b/src/Core/Services/VaultTimeoutService.cs index b6768e602..43adc7866 100644 --- a/src/Core/Services/VaultTimeoutService.cs +++ b/src/Core/Services/VaultTimeoutService.cs @@ -1,8 +1,7 @@ -using Bit.Core.Abstractions; -using Bit.Core.Models.Domain; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Enums; namespace Bit.Core.Services @@ -10,9 +9,8 @@ namespace Bit.Core.Services public class VaultTimeoutService : IVaultTimeoutService { private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IPlatformUtilsService _platformUtilsService; - private readonly IStorageService _storageService; private readonly IFolderService _folderService; private readonly ICipherService _cipherService; private readonly ICollectionService _collectionService; @@ -21,14 +19,13 @@ namespace Bit.Core.Services private readonly ITokenService _tokenService; private readonly IPolicyService _policyService; private readonly IKeyConnectorService _keyConnectorService; - private readonly Action _lockedCallback; - private readonly Func _loggedOutCallback; + private readonly Func, Task> _lockedCallback; + private readonly Func, Task> _loggedOutCallback; public VaultTimeoutService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IPlatformUtilsService platformUtilsService, - IStorageService storageService, IFolderService folderService, ICipherService cipherService, ICollectionService collectionService, @@ -37,13 +34,12 @@ namespace Bit.Core.Services ITokenService tokenService, IPolicyService policyService, IKeyConnectorService keyConnectorService, - Action lockedCallback, - Func loggedOutCallback) + Func, Task> lockedCallback, + Func, Task> loggedOutCallback) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _platformUtilsService = platformUtilsService; - _storageService = storageService; _folderService = folderService; _cipherService = cipherService; _collectionService = collectionService; @@ -56,17 +52,15 @@ namespace Bit.Core.Services _loggedOutCallback = loggedOutCallback; } - public EncString PinProtectedKey { get; set; } = null; - public bool BiometricLocked { get; set; } = true; public long? DelayLockAndLogoutMs { get; set; } - public async Task IsLockedAsync() + public async Task IsLockedAsync(string userId = null) { - var hasKey = await _cryptoService.HasKeyAsync(); + var hasKey = await _cryptoService.HasKeyAsync(userId); if (hasKey) { - var biometricSet = await IsBiometricLockSetAsync(); - if (biometricSet && BiometricLocked) + var biometricSet = await IsBiometricLockSetAsync(userId); + if (biometricSet && await _stateService.GetBiometricLockedAsync(userId)) { return true; } @@ -74,145 +68,184 @@ namespace Bit.Core.Services return !hasKey; } + public async Task ShouldLockAsync(string userId = null) + { + return await ShouldTimeoutAsync(userId) + && + await _stateService.GetVaultTimeoutActionAsync(userId) == VaultTimeoutAction.Lock; + } + + public async Task IsLoggedOutByTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); + var email = await _stateService.GetEmailAsync(userId); + return !authed && !string.IsNullOrWhiteSpace(email); + } + + public async Task ShouldLogOutByTimeoutAsync(string userId = null) + { + return await ShouldTimeoutAsync(userId) + && + await _stateService.GetVaultTimeoutActionAsync(userId) == VaultTimeoutAction.Logout; + } + public async Task CheckVaultTimeoutAsync() { if (_platformUtilsService.IsViewOpen()) { return; } - var authed = await _userService.IsAuthenticatedAsync(); + + if (await ShouldTimeoutAsync()) + { + await ExecuteTimeoutActionAsync(); + } + } + + public async Task ShouldTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); if (!authed) { - return; + return false; } - if (await IsLockedAsync()) + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userId); + if (vaultTimeoutAction == VaultTimeoutAction.Lock && await IsLockedAsync(userId)) { - return; + return false; } - var vaultTimeoutMinutes = await GetVaultTimeout(); + var vaultTimeoutMinutes = await GetVaultTimeout(userId); if (vaultTimeoutMinutes < 0 || vaultTimeoutMinutes == null) { - return; + return false; } if (vaultTimeoutMinutes == 0 && !DelayLockAndLogoutMs.HasValue) { - await LockOrLogout(); + return true; } - var lastActiveTime = await _storageService.GetAsync(Constants.LastActiveTimeKey); + var lastActiveTime = await _stateService.GetLastActiveTimeAsync(userId); if (lastActiveTime == null) { - return; + return false; } var diffMs = _platformUtilsService.GetActiveTime() - lastActiveTime; if (DelayLockAndLogoutMs.HasValue && diffMs < DelayLockAndLogoutMs) { - return; + return false; } var vaultTimeoutMs = vaultTimeoutMinutes * 60000; - if (diffMs >= vaultTimeoutMs) - { - await LockOrLogout(); - } - + return diffMs >= vaultTimeoutMs; } - private async Task LockOrLogout() + public async Task ExecuteTimeoutActionAsync(string userId = null) { - // Pivot based on saved action - var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (action == "logOut") + var action = await _stateService.GetVaultTimeoutActionAsync(userId); + if (action == VaultTimeoutAction.Logout) { - await LogOutAsync(); + await LogOutAsync(false, userId); } else { - await LockAsync(true); + await LockAsync(true, false, userId); } } - public async Task LockAsync(bool allowSoftLock = false, bool userInitiated = false) + public async Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null) { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(userId); if (!authed) { return; } - if (await _keyConnectorService.GetUsesKeyConnector()) { - var pinSet = await IsPinLockSetAsync(); - var pinLock = (pinSet.Item1 && PinProtectedKey != null) || pinSet.Item2; + var isActiveAccount = await _stateService.IsActiveAccountAsync(userId); + + if (userId == null) + { + userId = await _stateService.GetActiveUserIdAsync(); + } + + if (await _keyConnectorService.GetUsesKeyConnector()) + { + var (isPinProtected, isPinProtectedWithKey) = await IsPinLockSetAsync(userId); + var pinLock = (isPinProtected && await _stateService.GetPinProtectedKeyAsync(userId) != null) || + isPinProtectedWithKey; if (!pinLock && !await IsBiometricLockSetAsync()) { - await LogOutAsync(); + await LogOutAsync(userInitiated, userId); return; } } if (allowSoftLock) { - BiometricLocked = await IsBiometricLockSetAsync(); - if (BiometricLocked) + var isBiometricLockSet = await IsBiometricLockSetAsync(userId); + await _stateService.SetBiometricLockedAsync(isBiometricLockSet, userId); + if (isBiometricLockSet) { - _messagingService.Send("locked", userInitiated); - _lockedCallback?.Invoke(userInitiated); + _lockedCallback?.Invoke(new Tuple(userId, userInitiated)); return; } } await Task.WhenAll( - _cryptoService.ClearKeyAsync(), - _cryptoService.ClearOrgKeysAsync(true), - _cryptoService.ClearKeyPairAsync(true), - _cryptoService.ClearEncKeyAsync(true)); + _cryptoService.ClearKeyAsync(userId), + _cryptoService.ClearOrgKeysAsync(true, userId), + _cryptoService.ClearKeyPairAsync(true, userId), + _cryptoService.ClearEncKeyAsync(true, userId)); - _folderService.ClearCache(); - await _cipherService.ClearCacheAsync(); - _collectionService.ClearCache(); - _searchService.ClearIndex(); - _messagingService.Send("locked", userInitiated); - _lockedCallback?.Invoke(userInitiated); - } - - public async Task LogOutAsync() - { - if(_loggedOutCallback != null) + if (isActiveAccount) { - await _loggedOutCallback.Invoke(false); + _folderService.ClearCache(); + await _cipherService.ClearCacheAsync(); + _collectionService.ClearCache(); + _searchService.ClearIndex(); + } + _lockedCallback?.Invoke(new Tuple(userId, userInitiated)); + } + + public async Task LogOutAsync(bool userInitiated = true, string userId = null) + { + if (_loggedOutCallback != null) + { + await _loggedOutCallback.Invoke(new Tuple(userId, userInitiated, false)); } } - public async Task SetVaultTimeoutOptionsAsync(int? timeout, string action) + public async Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action) { - await _storageService.SaveAsync(Constants.VaultTimeoutKey, timeout); - await _storageService.SaveAsync(Constants.VaultTimeoutActionKey, action); + await _stateService.SetVaultTimeoutAsync(timeout); + await _stateService.SetVaultTimeoutActionAsync(action); await _cryptoService.ToggleKeyAsync(); await _tokenService.ToggleTokensAsync(); } - public async Task> IsPinLockSetAsync() + public async Task> IsPinLockSetAsync(string userId = null) { - var protectedPin = await _storageService.GetAsync(Constants.ProtectedPin); - var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + var protectedPin = await _stateService.GetProtectedPinAsync(userId); + var pinProtectedKey = await _stateService.GetPinProtectedAsync(userId); return new Tuple(protectedPin != null, pinProtectedKey != null); } - public async Task IsBiometricLockSetAsync() + public async Task IsBiometricLockSetAsync(string userId = null) { - var biometricLock = await _storageService.GetAsync(Constants.BiometricUnlockKey); + var biometricLock = await _stateService.GetBiometricUnlockAsync(userId); return biometricLock.GetValueOrDefault(); } - public async Task ClearAsync() + public async Task ClearAsync(string userId = null) { - PinProtectedKey = null; - await _storageService.RemoveAsync(Constants.ProtectedPin); + await _stateService.SetPinProtectedKeyAsync(null, userId); + await _stateService.SetProtectedPinAsync(null, userId); } - public async Task GetVaultTimeout() { - var vaultTimeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); + public async Task GetVaultTimeout(string userId = null) + { + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(userId); - if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout)) { - var policy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout)).First(); + if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)) + { + var policy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout, userId)).First(); // Remove negative values, and ensure it's smaller than maximum allowed value according to policy var policyTimeout = _policyService.GetPolicyInt(policy, "minutes"); if (!policyTimeout.HasValue) @@ -222,13 +255,15 @@ namespace Bit.Core.Services var timeout = vaultTimeout.HasValue ? Math.Min(vaultTimeout.Value, policyTimeout.Value) : policyTimeout.Value; - if (timeout < 0) { + if (timeout < 0) + { timeout = policyTimeout.Value; } // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (vaultTimeout != timeout) { - await _storageService.SaveAsync(Constants.VaultTimeoutKey, timeout); + if (vaultTimeout != timeout) + { + await _stateService.SetVaultTimeoutAsync(timeout, userId); } return timeout; diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index ca32ad0a9..f871c54ff 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -1,9 +1,9 @@ -using Bit.Core.Models.Domain; -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Bit.Core.Models.Domain; +using Newtonsoft.Json; namespace Bit.Core.Utilities { @@ -194,7 +194,7 @@ namespace Bit.Core.Utilities } return JsonConvert.SerializeObject(obj, jsonSerializationSettings); } - + public static string SerializeJson(object obj, JsonSerializerSettings jsonSerializationSettings) { return JsonConvert.SerializeObject(obj, jsonSerializationSettings); diff --git a/src/Core/Utilities/ExtendedViewModel.cs b/src/Core/Utilities/ExtendedViewModel.cs index fd394ea34..ed586d453 100644 --- a/src/Core/Utilities/ExtendedViewModel.cs +++ b/src/Core/Utilities/ExtendedViewModel.cs @@ -10,7 +10,7 @@ namespace Bit.Core.Utilities public event PropertyChangedEventHandler PropertyChanged; protected bool SetProperty(ref T backingStore, T value, Action onChanged = null, - [CallerMemberName]string propertyName = "", string[] additionalPropertyNames = null) + [CallerMemberName] string propertyName = "", string[] additionalPropertyNames = null) { if (EqualityComparer.Default.Equals(backingStore, value)) { diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index dcafe40c5..19450173b 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -1,8 +1,8 @@ -using Bit.Core.Abstractions; -using Bit.Core.Services; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Services; namespace Bit.Core.Utilities { @@ -11,7 +11,7 @@ namespace Bit.Core.Utilities public static Dictionary RegisteredServices { get; set; } = new Dictionary(); public static bool Inited { get; set; } - public static void Init(string customUserAgent = null, string clearCipherCacheKey = null, + public static void Init(string customUserAgent = null, string clearCipherCacheKey = null, string[] allClearCipherCacheKeys = null) { if (Inited) @@ -22,64 +22,71 @@ namespace Bit.Core.Utilities var platformUtilsService = Resolve("platformUtilsService"); var storageService = Resolve("storageService"); - var secureStorageService = Resolve("secureStorageService"); + var stateService = Resolve("stateService"); var i18nService = Resolve("i18nService"); var messagingService = Resolve("messagingService"); var cryptoFunctionService = Resolve("cryptoFunctionService"); var cryptoService = Resolve("cryptoService"); SearchService searchService = null; - var stateService = new StateService(); - var tokenService = new TokenService(storageService); - var apiService = new ApiService(tokenService, platformUtilsService, (bool expired) => + var tokenService = new TokenService(stateService); + var apiService = new ApiService(tokenService, platformUtilsService, (extras) => { - messagingService.Send("logout", expired); - return Task.FromResult(0); + messagingService.Send("logout", extras); + return Task.CompletedTask; }, customUserAgent); var appIdService = new AppIdService(storageService); - var userService = new UserService(storageService, tokenService); - var settingsService = new SettingsService(userService, storageService); + var organizationService = new OrganizationService(stateService); + var settingsService = new SettingsService(stateService); var fileUploadService = new FileUploadService(apiService); - var cipherService = new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => searchService, clearCipherCacheKey, allClearCipherCacheKeys); - var folderService = new FolderService(cryptoService, userService, apiService, storageService, - i18nService, cipherService); - var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService); - var sendService = new SendService(cryptoService, userService, apiService, fileUploadService, storageService, - i18nService, cryptoFunctionService); + var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService, + fileUploadService, storageService, i18nService, () => searchService, clearCipherCacheKey, + allClearCipherCacheKeys); + var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService); + var collectionService = new CollectionService(cryptoService, stateService, i18nService); + var sendService = new SendService(cryptoService, stateService, apiService, fileUploadService, i18nService, + cryptoFunctionService); searchService = new SearchService(cipherService, sendService); - var policyService = new PolicyService(storageService, userService); - var keyConnectorService = new KeyConnectorService(userService, cryptoService, storageService, tokenService, apiService); - var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService, - storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService, - policyService, keyConnectorService, null, (expired) => + var policyService = new PolicyService(stateService, organizationService); + var keyConnectorService = new KeyConnectorService(stateService, cryptoService, tokenService, apiService, + organizationService); + var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, + folderService, cipherService, collectionService, searchService, messagingService, tokenService, + policyService, keyConnectorService, + (extras) => { - messagingService.Send("logout", expired); - return Task.FromResult(0); - }); - var syncService = new SyncService(userService, apiService, settingsService, folderService, - cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService, - keyConnectorService, (bool expired) => + messagingService.Send("locked", extras); + return Task.CompletedTask; + }, + (extras) => { - messagingService.Send("logout", expired); - return Task.FromResult(0); + messagingService.Send("logout", extras); + return Task.CompletedTask; }); - var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, + var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService, + cryptoService, collectionService, organizationService, messagingService, policyService, sendService, + keyConnectorService, (extras) => + { + messagingService.Send("logout", extras); + return Task.CompletedTask; + }); + var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); - var totpService = new TotpService(storageService, cryptoFunctionService); - var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, userService, tokenService, appIdService, - i18nService, platformUtilsService, messagingService, vaultTimeoutService, keyConnectorService); + var totpService = new TotpService(stateService, cryptoFunctionService); + var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, + tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService, + keyConnectorService); var exportService = new ExportService(folderService, cipherService, cryptoService); var auditService = new AuditService(cryptoFunctionService, apiService); - var environmentService = new EnvironmentService(apiService, storageService); - var eventService = new EventService(storageService, apiService, userService, cipherService); - var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, cryptoService); + var environmentService = new EnvironmentService(apiService, stateService); + var eventService = new EventService(apiService, stateService, organizationService, cipherService); + var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, + cryptoService); - Register("stateService", stateService); Register("tokenService", tokenService); Register("apiService", apiService); Register("appIdService", appIdService); - Register("userService", userService); + Register("organizationService", organizationService); Register("settingsService", settingsService); Register("cipherService", cipherService); Register("folderService", folderService); diff --git a/src/Core/Utilities/TaskExtensions.cs b/src/Core/Utilities/TaskExtensions.cs index 8e3eb8a6f..2400671f6 100644 --- a/src/Core/Utilities/TaskExtensions.cs +++ b/src/Core/Utilities/TaskExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Threading.Tasks; -#if !FDROID -using Microsoft.AppCenter.Crashes; -#endif +using Bit.Core.Services; namespace Bit.Core.Utilities { @@ -22,9 +20,7 @@ namespace Bit.Core.Utilities } catch (Exception ex) { -#if !FDROID - Crashes.TrackError(ex); -#endif + LoggerHelper.LogEvenIfCantBeResolved(ex); onException?.Invoke(ex); } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 2a9fed346..e8ae87b4b 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -20,7 +20,6 @@ namespace Bit.iOS.Autofill public partial class CredentialProviderViewController : ASCredentialProviderViewController { private Context _context; - private bool _initedAppCenter; private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; @@ -79,9 +78,9 @@ namespace Bit.iOS.Autofill public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) { InitAppIfNeeded(); - var storageService = ServiceContainer.Resolve("storageService"); - await storageService.SaveAsync(Bit.Core.Constants.PasswordRepromptAutofillKey, false); - await storageService.SaveAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey, false); + var stateService = ServiceContainer.Resolve("stateService"); + await stateService.SetPasswordRepromptAutofillAsync(false); + await stateService.SetPasswordVerifiedAutofillAsync(false); if (!await IsAuthed() || await IsLocked()) { var err = new NSError(new NSString("ASExtensionErrorDomain"), @@ -230,7 +229,7 @@ namespace Bit.iOS.Autofill return; } - var storageService = ServiceContainer.Resolve("storageService"); + var stateService = ServiceContainer.Resolve("stateService"); var decCipher = await cipher.DecryptAsync(); if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None) { @@ -238,13 +237,13 @@ namespace Bit.iOS.Autofill // already verified the password. if (!userInteraction) { - await storageService.SaveAsync(Bit.Core.Constants.PasswordRepromptAutofillKey, true); + await stateService.SetPasswordRepromptAutofillAsync(true); var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); ExtensionContext?.CancelRequest(err); return; } - else if (!await storageService.GetAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey)) + else if (!await stateService.GetPasswordVerifiedAutofillAsync()) { // Add a timeout to resolve keyboard not always showing up. await Task.Delay(250); @@ -259,11 +258,10 @@ namespace Bit.iOS.Autofill } } string totpCode = null; - var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + var disableTotpCopy = await stateService.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + var canAccessPremiumAsync = await stateService.CanAccessPremiumAsync(); if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && (canAccessPremiumAsync || cipher.OrganizationUseTotp)) { @@ -277,8 +275,8 @@ namespace Bit.iOS.Autofill private async void CheckLock(Action notLockedAction) { - var storageService = ServiceContainer.Resolve("storageService"); - if (await IsLocked() || await storageService.GetAsync(Bit.Core.Constants.PasswordRepromptAutofillKey)) + var stateService = ServiceContainer.Resolve("stateService"); + if (await IsLocked() || await stateService.GetPasswordRepromptAutofillAsync()) { PerformSegue("lockPasswordSegue", this); } @@ -296,8 +294,8 @@ namespace Bit.iOS.Autofill private Task IsAuthed() { - var userService = ServiceContainer.Resolve("userService"); - return userService.IsAuthenticatedAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -306,7 +304,8 @@ namespace Bit.iOS.Autofill { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); if (deviceActionService.SystemMajorVersion() >= 12) { @@ -330,14 +329,10 @@ namespace Bit.iOS.Autofill var messagingService = ServiceContainer.Resolve("messagingService"); ServiceContainer.Init(deviceActionService.DeviceUserAgent, Bit.Core.Constants.iOSAutoFillClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); - if (!_initedAppCenter) - { - iOSCoreHelpers.RegisterAppCenter(); - _initedAppCenter = true; - } + iOSCoreHelpers.InitLogger(); iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new Core.NFCReaderDelegate((success, message) => messagingService.Send("gotYubiKeyOTP", message)); @@ -356,7 +351,7 @@ namespace Bit.iOS.Autofill { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -379,7 +374,7 @@ namespace Bit.iOS.Autofill { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -397,7 +392,7 @@ namespace Bit.iOS.Autofill { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -415,7 +410,7 @@ namespace Bit.iOS.Autofill { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -437,7 +432,7 @@ namespace Bit.iOS.Autofill { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -460,7 +455,7 @@ namespace Bit.iOS.Autofill { var twoFactorPage = new TwoFactorPage(authingWithSso); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -487,7 +482,7 @@ namespace Bit.iOS.Autofill { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -506,7 +501,7 @@ namespace Bit.iOS.Autofill { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS.Autofill/Info.plist b/src/iOS.Autofill/Info.plist index 580392bfe..306b1513c 100644 --- a/src/iOS.Autofill/Info.plist +++ b/src/iOS.Autofill/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.autofill CFBundleShortVersionString - 2.16.2 + 2022.05.1 CFBundleVersion 1 CFBundleLocalizations diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs index fa1895968..2f274b913 100644 --- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs +++ b/src/iOS.Autofill/Utilities/AutofillHelpers.cs @@ -41,12 +41,11 @@ namespace Bit.iOS.Autofill.Utilities if (!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password)) { string totp = null; - var storageService = ServiceContainer.Resolve("storageService"); - var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + var stateService = ServiceContainer.Resolve("stateService"); + var disableTotpCopy = await stateService.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + var canAccessPremiumAsync = await stateService.CanAccessPremiumAsync(); if (!string.IsNullOrWhiteSpace(item.Totp) && (canAccessPremiumAsync || item.CipherView.OrganizationUseTotp)) { @@ -118,4 +117,4 @@ namespace Bit.iOS.Autofill.Utilities } } } -} \ No newline at end of file +} diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index 2a238111e..5f4bfcee4 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -23,13 +23,13 @@ namespace Bit.iOS.Core.Controllers private IVaultTimeoutService _vaultTimeoutService; private ICryptoService _cryptoService; private IDeviceActionService _deviceActionService; - private IUserService _userService; - private IStorageService _storageService; + private IStateService _stateService; private IStorageService _secureStorageService; private IPlatformUtilsService _platformUtilsService; private IBiometricService _biometricService; private IKeyConnectorService _keyConnectorService; - private Tuple _pinSet; + private bool _isPinProtected; + private bool _isPinProtectedWithKey; private bool _pinLock; private bool _biometricLock; private bool _biometricIntegrityValid = true; @@ -84,8 +84,7 @@ namespace Bit.iOS.Core.Controllers _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _cryptoService = ServiceContainer.Resolve("cryptoService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _userService = ServiceContainer.Resolve("userService"); - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _biometricService = ServiceContainer.Resolve("biometricService"); @@ -93,21 +92,22 @@ namespace Bit.iOS.Core.Controllers // We re-use the lock screen for autofill extension to verify master password // when trying to access protected items. - if (autofillExtension && await _storageService.GetAsync(Bit.Core.Constants.PasswordRepromptAutofillKey)) + if (autofillExtension && await _stateService.GetPasswordRepromptAutofillAsync()) { _passwordReprompt = true; - _pinSet = Tuple.Create(false, false); + _isPinProtected = false; + _isPinProtectedWithKey = false; _pinLock = false; _biometricLock = false; } else { - _pinSet = _vaultTimeoutService.IsPinLockSetAsync().GetAwaiter().GetResult(); - _pinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2; - _biometricLock = _vaultTimeoutService.IsBiometricLockSetAsync().GetAwaiter().GetResult() && - _cryptoService.HasKeyAsync().GetAwaiter().GetResult(); - _biometricIntegrityValid = _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey).GetAwaiter() - .GetResult(); + (_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync(); + _pinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) || + _isPinProtectedWithKey; + _biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && + await _cryptoService.HasKeyAsync(); + _biometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey); _usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector(); _biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock; } @@ -213,9 +213,9 @@ namespace Bit.iOS.Core.Controllers return; } - var email = await _userService.GetEmailAsync(); - var kdf = await _userService.GetKdfAsync(); - var kdfIterations = await _userService.GetKdfIterationsAsync(); + var email = await _stateService.GetEmailAsync(); + var kdf = await _stateService.GetKdfTypeAsync(); + var kdfIterations = await _stateService.GetKdfIterationsAsync(); var inputtedValue = MasterPasswordCell.TextField.Text; if (_pinLock) @@ -223,13 +223,13 @@ namespace Bit.iOS.Core.Controllers var failed = true; try { - if (_pinSet.Item1) + if (_isPinProtected) { var key = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email, kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000), - _vaultTimeoutService.PinProtectedKey); + await _stateService.GetPinProtectedKeyAsync()); var encKey = await _cryptoService.GetEncKeyAsync(key); - var protectedPin = await _storageService.GetAsync(Bit.Core.Constants.ProtectedPin); + var protectedPin = await _stateService.GetProtectedPinAsync(); var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); failed = decPin != inputtedValue; if (!failed) @@ -280,14 +280,14 @@ namespace Bit.iOS.Core.Controllers var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, key2); if (passwordValid) { - if (_pinSet.Item1) + if (_isPinProtected) { - var protectedPin = await _storageService.GetAsync(Bit.Core.Constants.ProtectedPin); + var protectedPin = await _stateService.GetProtectedPinAsync(); var encKey = await _cryptoService.GetEncKeyAsync(key2); var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, email, kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); - _vaultTimeoutService.PinProtectedKey = await _cryptoService.EncryptAsync(key2.Key, pinKey); + await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key2.Key, pinKey)); } await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await SetKeyAndContinueAsync(key2, true); @@ -314,7 +314,7 @@ namespace Bit.iOS.Core.Controllers var success = await _platformUtilsService.AuthenticateBiometricAsync(null, _pinLock ? AppResources.PIN : AppResources.MasterPassword, () => MasterPasswordCell.TextField.BecomeFirstResponder()); - _vaultTimeoutService.BiometricLocked = !success; + await _stateService.SetBiometricLockedAsync(!success); if (success) { DoContinue(); @@ -325,7 +325,7 @@ namespace Bit.iOS.Core.Controllers { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -353,10 +353,10 @@ namespace Bit.iOS.Core.Controllers { if (masterPassword) { - await _storageService.SaveAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey, true); + await _stateService.SetPasswordVerifiedAutofillAsync(true); } await EnableBiometricsIfNeeded(); - _vaultTimeoutService.BiometricLocked = false; + await _stateService.SetBiometricLockedAsync(false); MasterPasswordCell.TextField.ResignFirstResponder(); Success(); } @@ -385,7 +385,7 @@ namespace Bit.iOS.Core.Controllers private async Task LogOutAsync() { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync()); var authService = ServiceContainer.Resolve("authService"); authService.LogOut(() => { diff --git a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs index ba235edfd..17bf8a911 100644 --- a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs +++ b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs @@ -101,7 +101,7 @@ namespace Bit.iOS.Core.Controllers MinNumbersCell.Value = options.MinNumber.GetValueOrDefault(1); MinSpecialCell.Value = options.MinSpecial.GetValueOrDefault(1); LengthCell.Value = options.Length.GetValueOrDefault(14); - AmbiguousCell.Switch.On = options.Ambiguous.GetValueOrDefault(); + AmbiguousCell.Switch.On = !options.AllowAmbiguousChar.GetValueOrDefault(); NumWordsCell.Value = options.NumWords.GetValueOrDefault(3); WordSeparatorCell.TextField.Text = options.WordSeparator ?? ""; @@ -219,7 +219,7 @@ namespace Bit.iOS.Core.Controllers Special = SpecialCell.Switch.On, MinSpecial = MinSpecialCell.Value, MinNumber = MinNumbersCell.Value, - Ambiguous = AmbiguousCell.Switch.On, + AllowAmbiguousChar = !AmbiguousCell.Switch.On, NumWords = NumWordsCell.Value, WordSeparator = WordSeparatorCell.TextField.Text, Capitalize = CapitalizeCell.Switch.On, diff --git a/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs b/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs new file mode 100644 index 000000000..17450523c --- /dev/null +++ b/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs @@ -0,0 +1,37 @@ +using Bit.iOS.Core.Effects; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportEffect(typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), nameof(ScrollViewContentInsetAdjustmentBehaviorEffect))] +namespace Bit.iOS.Core.Effects +{ + public class ScrollViewContentInsetAdjustmentBehaviorEffect : PlatformEffect + { + protected override void OnAttached() + { + if (Element != null && Control is UIScrollView scrollView) + { + switch (App.Effects.ScrollViewContentInsetAdjustmentBehaviorEffect.GetContentInsetAdjustmentBehavior(Element)) + { + case App.Effects.ScrollContentInsetAdjustmentBehavior.Automatic: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Automatic; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.ScrollableAxes: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.ScrollableAxes; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.Never: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.Always: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Always; + break; + } + } + } + + protected override void OnDetached() + { + } + } +} diff --git a/src/iOS.Core/Renderers/CustomLabelRenderer.cs b/src/iOS.Core/Renderers/CustomLabelRenderer.cs index 19db3c40e..9daf4fa52 100644 --- a/src/iOS.Core/Renderers/CustomLabelRenderer.cs +++ b/src/iOS.Core/Renderers/CustomLabelRenderer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Bit.App.Controls; using Bit.iOS.Core.Renderers; using Bit.iOS.Core.Utilities; using UIKit; @@ -22,18 +23,7 @@ namespace Bit.iOS.Core.Renderers protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { - try - { - base.OnElementPropertyChanged(sender, e); - } - catch (NullReferenceException) - { - // Do nothing... - // There is an issue on Xamarin Forms which throws a null reference - // https://appcenter.ms/users/kspearrin/apps/bitwarden/crashes/errors/534094859u/overview - // TODO: Maybe something like this https://github.com/matteobortolazzo/HtmlLabelPlugin/pull/113 can be implemented to avoid this - // on html labels. - } + base.OnElementPropertyChanged(sender, e); if (e.PropertyName == Label.FontProperty.PropertyName || e.PropertyName == Label.TextProperty.PropertyName || @@ -48,10 +38,19 @@ namespace Bit.iOS.Core.Renderers if (Element is null || Control is null) return; - var pointSize = iOSHelpers.GetAccessibleFont
diff --git a/src/iOS.Extension/Info.plist b/src/iOS.Extension/Info.plist index f5101b359..9709f2139 100644 --- a/src/iOS.Extension/Info.plist +++ b/src/iOS.Extension/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.find-login-action-extension CFBundleShortVersionString - 2.16.2 + 2022.05.1 CFBundleLocalizations en diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 44acc0084..8fcf0da27 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -26,7 +26,6 @@ namespace Bit.iOS.Extension public partial class LoadingViewController : ExtendedUIViewController { private Context _context = new Context(); - private bool _initedAppCenter; private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; @@ -408,14 +407,10 @@ namespace Bit.iOS.Extension var messagingService = ServiceContainer.Resolve("messagingService"); ServiceContainer.Init(deviceActionService.DeviceUserAgent, Bit.Core.Constants.iOSExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); - if (!_initedAppCenter) - { - iOSCoreHelpers.RegisterAppCenter(); - _initedAppCenter = true; - } + iOSCoreHelpers.InitLogger(); iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new NFCReaderDelegate((success, message) => messagingService.Send("gotYubiKeyOTP", message)); @@ -430,8 +425,8 @@ namespace Bit.iOS.Extension private Task IsAuthed() { - var userService = ServiceContainer.Resolve("userService"); - return userService.IsAuthenticatedAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -440,7 +435,8 @@ namespace Bit.iOS.Extension { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); if (deviceActionService.SystemMajorVersion() >= 12) { @@ -454,7 +450,7 @@ namespace Bit.iOS.Extension { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -477,7 +473,7 @@ namespace Bit.iOS.Extension { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -495,7 +491,7 @@ namespace Bit.iOS.Extension { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -513,7 +509,7 @@ namespace Bit.iOS.Extension { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -535,7 +531,7 @@ namespace Bit.iOS.Extension { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -558,7 +554,7 @@ namespace Bit.iOS.Extension { var twoFactorPage = new TwoFactorPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -585,7 +581,7 @@ namespace Bit.iOS.Extension { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -604,7 +600,7 @@ namespace Bit.iOS.Extension { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs index 2dd2f7455..3df4654d4 100644 --- a/src/iOS.Extension/LoginListViewController.cs +++ b/src/iOS.Extension/LoginListViewController.cs @@ -9,8 +9,8 @@ using MobileCoreServices; using Bit.iOS.Core.Controllers; using Bit.App.Resources; using Bit.iOS.Core.Views; +using Bit.App.Abstractions; using Bit.Core.Utilities; -using Bit.Core.Abstractions; namespace Bit.iOS.Extension { @@ -20,10 +20,12 @@ namespace Bit.iOS.Extension : base(handle) { DismissModalAction = Cancel; + PasswordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); } public Context Context { get; set; } public LoadingViewController LoadingController { get; set; } + public IPasswordRepromptService PasswordRepromptService { get; private set; } public async override void ViewDidLoad() { @@ -106,7 +108,7 @@ namespace Bit.iOS.Extension _controller = controller; } - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) { tableView.DeselectRow(indexPath, true); tableView.EndEditing(true); @@ -124,12 +126,15 @@ namespace Bit.iOS.Extension return; } + if (item.Reprompt != Bit.Core.Enums.CipherRepromptType.None && !await _controller.PasswordRepromptService.ShowPasswordPromptAsync()) + { + return; + } + if (_controller.CanAutoFill() && !string.IsNullOrWhiteSpace(item.Password)) { string totp = null; - var storageService = ServiceContainer.Resolve("storageService"); - var disableTotpCopy = storageService.GetAsync( - Bit.Core.Constants.DisableAutoTotpCopyKey).GetAwaiter().GetResult(); + var disableTotpCopy = _stateService.GetDisableAutoTotpCopyAsync().GetAwaiter().GetResult(); if (!disableTotpCopy.GetValueOrDefault(false)) { totp = GetTotpAsync(item).GetAwaiter().GetResult(); diff --git a/src/iOS.ShareExtension/Info.plist b/src/iOS.ShareExtension/Info.plist index b57b6f7a3..9071b815b 100644 --- a/src/iOS.ShareExtension/Info.plist +++ b/src/iOS.ShareExtension/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.16.2 + 2022.05.1 CFBundleVersion 1 MinimumOSVersion diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 651fe21ec..6411ac175 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -9,6 +9,7 @@ using Bit.App.Pages; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.iOS.Core; using Bit.iOS.Core.Controllers; @@ -17,7 +18,6 @@ using Bit.iOS.Core.Views; using Bit.iOS.ShareExtension.Models; using CoreNFC; using Foundation; -using Microsoft.AppCenter.Crashes; using MobileCoreServices; using UIKit; using Xamarin.Forms; @@ -27,11 +27,10 @@ namespace Bit.iOS.ShareExtension public partial class LoadingViewController : ExtendedUIViewController { private Context _context = new Context(); - private bool _initedAppCenter; private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; - readonly LazyResolve _userService = new LazyResolve("userService"); + readonly LazyResolve _stateService = new LazyResolve("stateService"); readonly LazyResolve _vaultTimeoutService = new LazyResolve("vaultTimeoutService"); readonly LazyResolve _deviceActionService = new LazyResolve("deviceActionService"); readonly LazyResolve _eventService = new LazyResolve("eventService"); @@ -99,7 +98,7 @@ namespace Bit.iOS.ShareExtension } catch (Exception ex) { - Crashes.TrackError(ex); + LoggerHelper.LogEvenIfCantBeResolved(ex); } } @@ -148,7 +147,7 @@ namespace Bit.iOS.ShareExtension }; var app = new App.App(appOptions); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(sendAddEditPage); var navigationPage = new NavigationPage(sendAddEditPage); @@ -215,16 +214,12 @@ namespace Bit.iOS.ShareExtension iOSCoreHelpers.RegisterLocalServices(); var messagingService = ServiceContainer.Resolve("messagingService"); ServiceContainer.Init(_deviceActionService.Value.DeviceUserAgent, - Bit.Core.Constants.iOSExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); - if (!_initedAppCenter) - { - iOSCoreHelpers.RegisterAppCenter(); - _initedAppCenter = true; - } + Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); + iOSCoreHelpers.InitLogger(); iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new NFCReaderDelegate((success, message) => @@ -239,7 +234,7 @@ namespace Bit.iOS.ShareExtension private Task IsAuthed() { - return _userService.Value.IsAuthenticatedAsync(); + return _stateService.Value.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -248,7 +243,7 @@ namespace Bit.iOS.ShareExtension { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); if (_deviceActionService.Value.SystemMajorVersion() >= 12) { await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); @@ -261,7 +256,7 @@ namespace Bit.iOS.ShareExtension { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -284,7 +279,7 @@ namespace Bit.iOS.ShareExtension { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -302,7 +297,7 @@ namespace Bit.iOS.ShareExtension { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -320,7 +315,7 @@ namespace Bit.iOS.ShareExtension { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -342,7 +337,7 @@ namespace Bit.iOS.ShareExtension { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -365,7 +360,7 @@ namespace Bit.iOS.ShareExtension { var twoFactorPage = new TwoFactorPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -392,7 +387,7 @@ namespace Bit.iOS.ShareExtension { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -411,7 +406,7 @@ namespace Bit.iOS.ShareExtension { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS.ShareExtension/LockPasswordViewController.cs b/src/iOS.ShareExtension/LockPasswordViewController.cs index 9ac9cfc36..7a1b599f7 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.cs @@ -9,7 +9,7 @@ namespace Bit.iOS.ShareExtension public LockPasswordViewController(IntPtr handle) : base(handle) { - BiometricIntegrityKey = Bit.Core.Constants.iOSExtensionBiometricIntegrityKey; + BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey; DismissModalAction = Cancel; } diff --git a/src/iOS.ShareExtension/iOS.ShareExtension.csproj b/src/iOS.ShareExtension/iOS.ShareExtension.csproj index 9b294eadd..124867571 100644 --- a/src/iOS.ShareExtension/iOS.ShareExtension.csproj +++ b/src/iOS.ShareExtension/iOS.ShareExtension.csproj @@ -167,7 +167,7 @@ - + 4.4.0 diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 48c1bdd64..020a3803b 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -8,6 +8,7 @@ using Bit.App.Services; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; @@ -23,6 +24,8 @@ namespace Bit.iOS [Register("AppDelegate")] public partial class AppDelegate : FormsApplicationDelegate { + const int SPLASH_VIEW_TAG = 4321; + private NFCNdefReaderSession _nfcSession = null; private iOSPushNotificationHandler _pushHandler = null; private Core.NFCReaderDelegate _nfcDelegate = null; @@ -35,7 +38,7 @@ namespace Bit.iOS private IMessagingService _messagingService; private IBroadcasterService _broadcasterService; private IStorageService _storageService; - private IVaultTimeoutService _vaultTimeoutService; + private IStateService _stateService; private IEventService _eventService; public override bool FinishedLaunching(UIApplication app, NSDictionary options) @@ -47,7 +50,7 @@ namespace Bit.iOS _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _storageService = ServiceContainer.Resolve("storageService"); - _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _stateService = ServiceContainer.Resolve("stateService"); _eventService = ServiceContainer.Resolve("eventService"); LoadApplication(new App.App(null)); @@ -88,11 +91,6 @@ namespace Bit.iOS { Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); } - else if (message.Command == "showStatusBar") - { - Device.BeginInvokeOnMainThread(() => - UIApplication.SharedApplication.SetStatusBarHidden(!(bool)message.Data, false)); - } else if (message.Command == "syncCompleted") { if (message.Data is Dictionary data && data.ContainsKey("successfully")) @@ -146,7 +144,7 @@ namespace Bit.iOS await ASHelpers.ReplaceAllIdentities(); } } - else if (message.Command == "loggedOut") + else if (message.Command == "logout") { if (_deviceActionService.SystemMajorVersion() >= 12) { @@ -160,8 +158,8 @@ namespace Bit.iOS } else if (message.Command == "vaultTimeoutActionChanged") { - var timeoutAction = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (timeoutAction == "logOut") + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) { await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); } @@ -179,7 +177,7 @@ namespace Bit.iOS { var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) { - Tag = 4321 + Tag = SPLASH_VIEW_TAG }; var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) { @@ -195,13 +193,12 @@ namespace Bit.iOS UIApplication.SharedApplication.KeyWindow.AddSubview(view); UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); UIApplication.SharedApplication.KeyWindow.EndEditing(true); - UIApplication.SharedApplication.SetStatusBarHidden(true, false); base.OnResignActivation(uiApplication); } public override void DidEnterBackground(UIApplication uiApplication) { - _storageService?.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); + _stateService?.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); _messagingService?.Send("slept"); base.DidEnterBackground(uiApplication); } @@ -210,12 +207,9 @@ namespace Bit.iOS { base.OnActivated(uiApplication); UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; - var view = UIApplication.SharedApplication.KeyWindow.ViewWithTag(4321); - if (view != null) - { - view.RemoveFromSuperview(); - UIApplication.SharedApplication.SetStatusBarHidden(false, false); - } + UIApplication.SharedApplication.KeyWindow? + .ViewWithTag(SPLASH_VIEW_TAG)? + .RemoveFromSuperview(); ThemeManager.UpdateThemeOnPagesAsync(); } @@ -234,11 +228,7 @@ namespace Bit.iOS public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) { - if (Xamarin.Essentials.Platform.OpenUrl(app, url, options)) - { - return true; - } - return base.OpenUrl(app, url, options); + return Xamarin.Essentials.Platform.OpenUrl(app, url, options); } public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, @@ -286,7 +276,7 @@ namespace Bit.iOS } // Migration services - ServiceContainer.Register("logService", new ConsoleLogService()); + ServiceContainer.Register("nativeLogService", new ConsoleLogService()); // Note: This might cause a race condition. Investigate more. Task.Run(() => @@ -304,7 +294,7 @@ namespace Bit.iOS var deviceActionService = ServiceContainer.Resolve("deviceActionService"); ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey, Constants.iOSAllClearCipherCacheKeys); - iOSCoreHelpers.RegisterAppCenter(); + iOSCoreHelpers.InitLogger(); _pushHandler = new iOSPushNotificationHandler( ServiceContainer.Resolve("pushNotificationListenerService")); _nfcDelegate = new Core.NFCReaderDelegate((success, message) => diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist index b4d0003e7..3cf733988 100644 --- a/src/iOS/Info.plist +++ b/src/iOS/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden CFBundleShortVersionString - 2.16.2 + 2022.05.1 CFBundleVersion 1 CFBundleIconName diff --git a/src/iOS/LaunchScreen.storyboard b/src/iOS/LaunchScreen.storyboard index 393e62a39..6cec090bc 100644 --- a/src/iOS/LaunchScreen.storyboard +++ b/src/iOS/LaunchScreen.storyboard @@ -88,4 +88,4 @@ - + \ No newline at end of file diff --git a/src/iOS/Resources/cog.png b/src/iOS/Resources/cog_environment.png similarity index 100% rename from src/iOS/Resources/cog.png rename to src/iOS/Resources/cog_environment.png diff --git a/src/iOS/Resources/cog@2x.png b/src/iOS/Resources/cog_environment@2x.png similarity index 100% rename from src/iOS/Resources/cog@2x.png rename to src/iOS/Resources/cog_environment@2x.png diff --git a/src/iOS/Resources/cog@3x.png b/src/iOS/Resources/cog_environment@3x.png similarity index 100% rename from src/iOS/Resources/cog@3x.png rename to src/iOS/Resources/cog_environment@3x.png diff --git a/src/iOS/Resources/cog_settings.png b/src/iOS/Resources/cog_settings.png new file mode 100644 index 000000000..95d6271b8 Binary files /dev/null and b/src/iOS/Resources/cog_settings.png differ diff --git a/src/iOS/Resources/cog_settings@2x.png b/src/iOS/Resources/cog_settings@2x.png new file mode 100644 index 000000000..8d884844f Binary files /dev/null and b/src/iOS/Resources/cog_settings@2x.png differ diff --git a/src/iOS/Resources/cog_settings@3x.png b/src/iOS/Resources/cog_settings@3x.png new file mode 100644 index 000000000..a46070614 Binary files /dev/null and b/src/iOS/Resources/cog_settings@3x.png differ diff --git a/src/iOS/Services/iOSPushNotificationHandler.cs b/src/iOS/Services/iOSPushNotificationHandler.cs index bfa982432..197a7fe1a 100644 --- a/src/iOS/Services/iOSPushNotificationHandler.cs +++ b/src/iOS/Services/iOSPushNotificationHandler.cs @@ -1,8 +1,8 @@ using System; using System.Diagnostics; using Bit.App.Abstractions; +using Bit.Core.Services; using Foundation; -using Microsoft.AppCenter.Crashes; using Newtonsoft.Json.Linq; using UserNotifications; using Xamarin.Forms; @@ -45,7 +45,7 @@ namespace Bit.iOS.Services } catch (Exception ex) { - Crashes.TrackError(ex); + Logger.Instance.Exception(ex); } } diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index d2caf681a..66c0d0b2d 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -143,6 +143,7 @@ + false @@ -155,19 +156,21 @@ - - - - - - + + + + + + + + - - + + @@ -180,7 +183,7 @@ - 1.7.0 + 1.7.2 diff --git a/store/apple/ta/copy.resx b/store/apple/ta/copy.resx index 60506fc0c..bcadc9c6f 100644 --- a/store/apple/ta/copy.resx +++ b/store/apple/ta/copy.resx @@ -122,15 +122,31 @@ Max 30 characters - உங்கள் எல்லா உள்நுழைவுகளையும் கடவுச்சொற்களையும் சேமிக்கவும் எல்லா சாதனங்களுக்கும் இடையில் ஒத்திசைக்கவும் Bitwarden எளிய பாதுகாப்பான வழி. Bitwarden செயலி நீட்டிப்பு சஃபாரி அல்லது குரோம் மூலம் எந்தவொரு வலைத்தளத்திலும் விரைவாக உள்நுழைய உங்களை அனுமதிக்கிறது மற்றும் நூற்றுக்கணக்கான பிற பிரபல செயலிகளால் ஆதரிக்கப்படுகிறது. + Bitwarden, Inc. 8Bit Solutions LLC இன் தாய் நிறுவனம். -கடவுச்சொல் திருட்டு ஒரு தீவிரமான பிரச்சினை. நீங்கள் பார்க்கும் வலைத்தளங்களும் செயலிகளும் தினந்தினம் தாக்குதலுக்கு உள்ளாகிறது. பாதுகாப்பு மீறல்கள் ஏற்பட்டு உங்கள் கடவுச்சொற்கள் திருட்டுபோகிறது. அக்கடவுச்சொற்களை மற்ற செயலிகளிலும் வலைத்தளங்களிலும் பயன்படுத்தீர்களானால், ஊடுருவர்கள் உங்கள் மின்னஞ்சல், வங்கி மற்றும் வேறு முக்கிய கணக்குகளை எளிதில் அணுக இயலும். +THE VERGE, U.S. NEWS & WORLD REPORT, CNET மற்றும் பலவற்றால் மீச்சிறந்த கடவுச்சொல் நிர்வாகி எனப் பெயரிடப்பட்டது. -நீங்கள் உருவாக்கும் ஒவ்வொரு கணக்கிற்கும் வேறுபட்ட, சீரற்று உருவாக்கப்பட்ட கடவுச்சொல்லைப் பயன்படுத்துமாறு பாதுகாப்பு நிபுணர்கள் பரிந்துரைக்கின்றனர். ஆனால் அந்த கடவுச்சொற்களை எவ்வாறு நிர்வகிப்பீர்கள்? உங்கள் கடவுச்சொற்களை உருவாக்குவது, சேமிப்பது மற்றும் அணுகுவதை Bitwarden எளிதாக்குகிறது. +வரம்பற்ற கடவுச்சொற்களை வரம்பற்ற சாதனங்களுக்கிடையே எங்கிருந்தும் நிர்வகி, தேக்கு, பாதுகாத்துவை மற்றும் பகிர்க. Bitwarden வீட்டிலும் வேலையிலும் எங்குமுள்ளரோருக்குத் திறந்த மூல கடவுச்சொல் நிர்வகிப்பு அளிக்கிறது. -பிட்வார்டன் உங்கள் உள்நுழைவுகளை உங்கள் எல்லா சாதனங்களிலும் ஒத்திசைக்கூடிய மறைகுறியாக்கப்பட்ட பெட்டகத்தில் சேமிக்கிறது. இது உங்கள் சாதனத்தை விட்டு வெளியேறுவதற்கு முன்பு முழுமையாக குறியாக்கபடுவதால் நீங்கள் மட்டுமே உங்கள் தரவை அணுக முடியும். விரும்பினாலும் Bitwarden குழுவினரால் கூட உங்கள் தரவைப் படிக்க முடியாது. உங்கள் தரவு AES-256 பிட் குறியாக்கம், சால்டட் ஹாஷிங் மற்றும் PBKDF2 SHA-256 உடன் மூடப்பட்டுள்ளது. +அடிக்கடி பார்வையிடும் எல்லா வலைத்தளங்களுக்கும் பாதுகாப்பு தேவை அடிப்படையில் வலிய,தனித்துவ,சீரற்ற கடவுச்சொற்களை உருவாக்குக. -Bitwarden திறந்த மூல மென்பொருளில் கவனம் செலுத்துகிறது. Bitwarden மூல குறியீடு கிட்ஹப்பில் ஹோஸ்ட் செய்யப்பட்டுள்ளது, மேலும் அனைவருக்கும் பிட்வார்டன் கோட்பேஸில் மதிப்பாய்வு செய்ய, தணிக்கை செய்ய மற்றும் பங்களிக்க இலவசம். +மறையாக்கிய கோப்பு மற்றும் வெற்றுரை தகவல்களை Bitwarden Send நேரடியாக எவருக்கும் விரைவாக அனுப்புகிறது. + +நீங்கள் பாதுகாப்பாக கடவுச்சொற்களை சகபாடிகளுடன் பகிரும்வண்ணம் நிறுவனங்களுக்கு Bitwarden அணிகள் மற்றும் முனைவகத் திட்டங்கள் அளிக்கிறது. + +ஏன் Bitwarden தேர்வுசெய்வது: + +உலகத்தர மறையாக்கம் +உம் தரவு பாதுகாப்பாகவும் தனிப்பட்டதாகவுமிருக்க மேம்பட்ட இறுதிக்கிறுதி மறையாக்கத்துடன் கடவுச்சொற்கள் காக்கப்பட்டுள்ளன (AES-256 நுண்மி, salted hashing, மற்றும் PBKDF2 SHA-256). + +சேரக்கட்டிய கடவுச்சொல் உருவாக்கி +அடிக்கடி பார்வையிடும் எல்லா வலைத்தளங்களுக்கும் பாதுகாப்பு தேவை அடிப்படையில் வலிய,தனித்துவ,சீரற்ற கடவுச்சொற்களை உருவாக்குக. + +உலகளாவிய மொழிபெயர்ப்புகள் +Bitwarden மொழிபெயர்ப்புகள் 40 மொழிகளிலுள்ளன மற்றும் வளர்கிறது, நம் உலக சமூகக்குழுவிற்கு நன்றி. + +குறுக்கியக்குத்தள செயலிகள் +எந்த உலாவி,கைபேசி சாதனம், அல்லது மேசைத்தள OS முதலியவற்றிலிருந்தும் Bitwarden பெட்டகத்துடன் உணர்ச்சிவச தரவை காத்துப் பகிர்க. Max 4000 characters diff --git a/store/google/Publisher/Program.cs b/store/google/Publisher/Program.cs index 68cec9356..bbcb479bc 100644 --- a/store/google/Publisher/Program.cs +++ b/store/google/Publisher/Program.cs @@ -1,11 +1,11 @@ -using Google.Apis.AndroidPublisher.v3; -using Google.Apis.AndroidPublisher.v3.Data; -using Google.Apis.Auth.OAuth2; -using Google.Apis.Services; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Google.Apis.AndroidPublisher.v3; +using Google.Apis.AndroidPublisher.v3.Data; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Services; namespace Bit.Publisher { diff --git a/store/google/Publisher/Publisher.csproj b/store/google/Publisher/Publisher.csproj index c4eb120e0..bedaef882 100644 --- a/store/google/Publisher/Publisher.csproj +++ b/store/google/Publisher/Publisher.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 Bit.Publisher Debug;Release;FDroid diff --git a/store/google/ta/copy.resx b/store/google/ta/copy.resx index c4a0e2465..836b2f0ec 100644 --- a/store/google/ta/copy.resx +++ b/store/google/ta/copy.resx @@ -126,15 +126,31 @@ Max 80 characters - உங்கள் எல்லா உள்நுழைவுகளையும் கடவுச்சொற்களையும் சேமிக்கவும் எல்லா சாதனங்களுக்கும் இடையில் ஒத்திசைக்கவும் Bitwarden எளிய பாதுகாப்பான வழி. Bitwarden செயலி நீட்டிப்பு சஃபாரி அல்லது குரோம் மூலம் எந்தவொரு வலைத்தளத்திலும் விரைவாக உள்நுழைய உங்களை அனுமதிக்கிறது மற்றும் நூற்றுக்கணக்கான பிற பிரபல செயலிகளால் ஆதரிக்கப்படுகிறது. + Bitwarden, Inc. 8Bit Solutions LLC இன் தாய் நிறுவனம். -கடவுச்சொல் திருட்டு ஒரு தீவிரமான பிரச்சினை. நீங்கள் பார்க்கும் வலைத்தளங்களும் செயலிகளும் தினந்தினம் தாக்குதலுக்கு உள்ளாகிறது. பாதுகாப்பு மீறல்கள் ஏற்பட்டு உங்கள் கடவுச்சொற்கள் திருட்டுபோகிறது. அக்கடவுச்சொற்களை மற்ற செயலிகளிலும் வலைத்தளங்களிலும் பயன்படுத்தீர்களானால், ஊடுருவர்கள் உங்கள் மின்னஞ்சல், வங்கி மற்றும் வேறு முக்கிய கணக்குகளை எளிதில் அணுக இயலும். +THE VERGE, U.S. NEWS & WORLD REPORT, CNET மற்றும் பலவற்றால் மீச்சிறந்த கடவுச்சொல் நிர்வாகி எனப் பெயரிடப்பட்டது. -நீங்கள் உருவாக்கும் ஒவ்வொரு கணக்கிற்கும் வேறுபட்ட, சீரற்று உருவாக்கப்பட்ட கடவுச்சொல்லைப் பயன்படுத்துமாறு பாதுகாப்பு நிபுணர்கள் பரிந்துரைக்கின்றனர். ஆனால் அந்த கடவுச்சொற்களை எவ்வாறு நிர்வகிப்பீர்கள்? உங்கள் கடவுச்சொற்களை உருவாக்குவது, சேமிப்பது மற்றும் அணுகுவதை Bitwarden எளிதாக்குகிறது. +வரம்பற்ற கடவுச்சொற்களை வரம்பற்ற சாதனங்களுக்கிடையே எங்கிருந்தும் நிர்வகி, தேக்கு, பாதுகாத்துவை மற்றும் பகிர்க. Bitwarden வீட்டிலும் வேலையிலும் எங்குமுள்ளரோருக்குத் திறந்த மூல கடவுச்சொல் நிர்வகிப்பு அளிக்கிறது. -பிட்வார்டன் உங்கள் உள்நுழைவுகளை உங்கள் எல்லா சாதனங்களிலும் ஒத்திசைக்கூடிய மறைகுறியாக்கப்பட்ட பெட்டகத்தில் சேமிக்கிறது. இது உங்கள் சாதனத்தை விட்டு வெளியேறுவதற்கு முன்பு முழுமையாக குறியாக்கபடுவதால் நீங்கள் மட்டுமே உங்கள் தரவை அணுக முடியும். விரும்பினாலும் Bitwarden குழுவினரால் கூட உங்கள் தரவைப் படிக்க முடியாது. உங்கள் தரவு AES-256 பிட் குறியாக்கம், சால்டட் ஹாஷிங் மற்றும் PBKDF2 SHA-256 உடன் மூடப்பட்டுள்ளது. +அடிக்கடி பார்வையிடும் எல்லா வலைத்தளங்களுக்கும் பாதுகாப்பு தேவை அடிப்படையில் வலிய,தனித்துவ,சீரற்ற கடவுச்சொற்களை உருவாக்குக. -Bitwarden திறந்த மூல மென்பொருளில் கவனம் செலுத்துகிறது. Bitwarden மூல குறியீடு கிட்ஹப்பில் ஹோஸ்ட் செய்யப்பட்டுள்ளது, மேலும் அனைவருக்கும் பிட்வார்டன் கோட்பேஸில் மதிப்பாய்வு செய்ய, தணிக்கை செய்ய மற்றும் பங்களிக்க இலவசம். +மறையாக்கிய கோப்பு மற்றும் வெற்றுரை தகவல்களை Bitwarden Send நேரடியாக எவருக்கும் விரைவாக அனுப்புகிறது. + +நீங்கள் பாதுகாப்பாக கடவுச்சொற்களை சகபாடிகளுடன் பகிரும்வண்ணம் நிறுவனங்களுக்கு Bitwarden அணிகள் மற்றும் முனைவகத் திட்டங்கள் அளிக்கிறது. + +ஏன் Bitwarden தேர்வுசெய்வது: + +உலகத்தர மறையாக்கம் +உம் தரவு பாதுகாப்பாகவும் தனிப்பட்டதாகவுமிருக்க மேம்பட்ட இறுதிக்கிறுதி மறையாக்கத்துடன் கடவுச்சொற்கள் காக்கப்பட்டுள்ளன (AES-256 நுண்மி, salted hashing, மற்றும் PBKDF2 SHA-256). + +சேரக்கட்டிய கடவுச்சொல் உருவாக்கி +அடிக்கடி பார்வையிடும் எல்லா வலைத்தளங்களுக்கும் பாதுகாப்பு தேவை அடிப்படையில் வலிய,தனித்துவ,சீரற்ற கடவுச்சொற்களை உருவாக்குக. + +உலகளாவிய மொழிபெயர்ப்புகள் +Bitwarden மொழிபெயர்ப்புகள் 40 மொழிகளிலுள்ளன மற்றும் வளர்கிறது, நம் உலக சமூகக்குழுவிற்கு நன்றி. + +குறுக்கியக்குத்தள செயலிகள் +எந்த உலாவி,கைபேசி சாதனம், அல்லது மேசைத்தள OS முதலியவற்றிலிருந்தும் Bitwarden பெட்டகத்துடன் உணர்ச்சிவச தரவை காத்துப் பகிர்க. Max 4000 characters diff --git a/test/Common/AutoFixture/Attributes/AutoSubDataAttribute.cs b/test/Common/AutoFixture/Attributes/AutoSubDataAttribute.cs index fc4db5fe9..3e01d420e 100644 --- a/test/Common/AutoFixture/Attributes/AutoSubDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/AutoSubDataAttribute.cs @@ -1,4 +1,4 @@ -using AutoFixture.AutoNSubstitute; +using AutoFixture.AutoNSubstitute; namespace Bit.Test.Common.AutoFixture.Attributes { diff --git a/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs index cd2feebca..d6c5863b1 100644 --- a/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AutoFixture; using AutoFixture.Xunit2; diff --git a/test/Common/AutoFixture/Attributes/InlineAutoSubDataAttribute.cs b/test/Common/AutoFixture/Attributes/InlineAutoSubDataAttribute.cs index feccc4e76..b74f1ba02 100644 --- a/test/Common/AutoFixture/Attributes/InlineAutoSubDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/InlineAutoSubDataAttribute.cs @@ -1,4 +1,4 @@ -using AutoFixture.AutoNSubstitute; +using AutoFixture.AutoNSubstitute; namespace Bit.Test.Common.AutoFixture.Attributes { diff --git a/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs index d36f963a4..9a9a6730a 100644 --- a/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; +using AutoFixture; +using AutoFixture.Xunit2; using Xunit; using Xunit.Sdk; -using AutoFixture.Xunit2; -using AutoFixture; namespace Bit.Test.Common.AutoFixture.Attributes { diff --git a/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs index 89eebad8c..cc04e062a 100644 --- a/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AutoFixture; diff --git a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs index e7c06a88f..2a9545110 100644 --- a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; namespace Bit.Test.Common.AutoFixture.Attributes diff --git a/test/Common/AutoFixture/FixtureExtensions.cs b/test/Common/AutoFixture/FixtureExtensions.cs index a23eb7d0c..764a38c81 100644 --- a/test/Common/AutoFixture/FixtureExtensions.cs +++ b/test/Common/AutoFixture/FixtureExtensions.cs @@ -1,4 +1,4 @@ -using AutoFixture; +using AutoFixture; using AutoFixture.AutoNSubstitute; namespace Bit.Test.Common.AutoFixture diff --git a/test/Common/AutoFixture/ISutProvider.cs b/test/Common/AutoFixture/ISutProvider.cs index c72dc4a27..cbd8c6e6f 100644 --- a/test/Common/AutoFixture/ISutProvider.cs +++ b/test/Common/AutoFixture/ISutProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Bit.Test.Common.AutoFixture { diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index e3149afc6..989bc590d 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using AutoFixture; using AutoFixture.Kernel; -using System.Reflection; -using System.Linq; namespace Bit.Test.Common.AutoFixture { diff --git a/test/Common/AutoFixture/SutProviderCustomization.cs b/test/Common/AutoFixture/SutProviderCustomization.cs index b5bc84d1d..c323ed4c5 100644 --- a/test/Common/AutoFixture/SutProviderCustomization.cs +++ b/test/Common/AutoFixture/SutProviderCustomization.cs @@ -1,4 +1,4 @@ -using System; +using System; using AutoFixture; using AutoFixture.Kernel; diff --git a/test/Common/TestHelper.cs b/test/Common/TestHelper.cs index 7ce60f325..c70373591 100644 --- a/test/Common/TestHelper.cs +++ b/test/Common/TestHelper.cs @@ -1,9 +1,9 @@ -using System.Reflection; +using System; using System.IO; using System.Linq; -using Xunit; -using System; +using System.Reflection; using Newtonsoft.Json; +using Xunit; namespace Bit.Test.Common { diff --git a/test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs b/test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs index 8f745ebea..6d4f47aad 100644 --- a/test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs +++ b/test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs @@ -1,4 +1,4 @@ -using System; +using System; using AutoFixture; using Bit.Core.Models.Domain; using Bit.Test.Common.AutoFixture; diff --git a/test/Core.Test/AutoFixture/Domain/SymmetricCryptoKeyCustomization.cs b/test/Core.Test/AutoFixture/Domain/SymmetricCryptoKeyCustomization.cs index 41355b86d..742171a1f 100644 --- a/test/Core.Test/AutoFixture/Domain/SymmetricCryptoKeyCustomization.cs +++ b/test/Core.Test/AutoFixture/Domain/SymmetricCryptoKeyCustomization.cs @@ -1,4 +1,4 @@ -using System; +using System; using AutoFixture; using Bit.Core.Models.Domain; using Bit.Core.Services; diff --git a/test/Core.Test/AutoFixture/Send/SendCustomizations.cs b/test/Core.Test/AutoFixture/Send/SendCustomizations.cs index 617ba8c8e..a18f9707a 100644 --- a/test/Core.Test/AutoFixture/Send/SendCustomizations.cs +++ b/test/Core.Test/AutoFixture/Send/SendCustomizations.cs @@ -1,11 +1,11 @@ -using AutoFixture; +using AutoFixture; +using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Models.View; -using Bit.Core.Enums; namespace Bit.Core.Test.AutoFixture { diff --git a/test/Core.Test/Models/Data/SendDataTests.cs b/test/Core.Test/Models/Data/SendDataTests.cs index 06bb54b12..e456a6e00 100644 --- a/test/Core.Test/Models/Data/SendDataTests.cs +++ b/test/Core.Test/Models/Data/SendDataTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Test.AutoFixture; using Bit.Test.Common; diff --git a/test/Core.Test/Models/Domain/SendTests.cs b/test/Core.Test/Models/Domain/SendTests.cs index ff9883829..b5d58c1e1 100644 --- a/test/Core.Test/Models/Domain/SendTests.cs +++ b/test/Core.Test/Models/Domain/SendTests.cs @@ -1,17 +1,17 @@ -using System; +using System; using System.Linq; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; -using Bit.Test.Common; using System.Text; +using AutoFixture.AutoNSubstitute; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Test.AutoFixture; using Bit.Core.Utilities; +using Bit.Test.Common; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -using Bit.Core.Test.AutoFixture; -using AutoFixture.AutoNSubstitute; namespace Bit.Core.Test.Models.Domain { diff --git a/test/Core.Test/Models/Request/SendRequestTests.cs b/test/Core.Test/Models/Request/SendRequestTests.cs index f2fa4d9bb..028dce25a 100644 --- a/test/Core.Test/Models/Request/SendRequestTests.cs +++ b/test/Core.Test/Models/Request/SendRequestTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; diff --git a/test/Core.Test/Services/CipherServiceTests.cs b/test/Core.Test/Services/CipherServiceTests.cs index cecda9bd9..494fa192c 100644 --- a/test/Core.Test/Services/CipherServiceTests.cs +++ b/test/Core.Test/Services/CipherServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -52,7 +52,7 @@ namespace Bit.Core.Test.Services .Returns(data); sutProvider.GetDependency().MakeEncKeyAsync(Arg.Any()).Returns(new Tuple(null, encKey)); sutProvider.GetDependency().PostCipherAttachmentAsync(cipher.Id, Arg.Any()) - .Throws(new ApiException(new ErrorResponse {StatusCode = statusCode})); + .Throws(new ApiException(new ErrorResponse { StatusCode = statusCode })); sutProvider.GetDependency().PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any()) .Returns(response); @@ -76,7 +76,7 @@ namespace Bit.Core.Test.Services sutProvider.GetDependency().PostCipherAttachmentAsync(cipher.Id, Arg.Any()) .Throws(expectedException); - var actualException = await Assert.ThrowsAsync(async () => + var actualException = await Assert.ThrowsAsync(async () => await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer)); Assert.Equal(expectedException.Error.StatusCode, actualException.Error.StatusCode); diff --git a/test/Core.Test/Services/CryptoFunctionServiceTests.cs b/test/Core.Test/Services/CryptoFunctionServiceTests.cs index b95335e17..eea1af98a 100644 --- a/test/Core.Test/Services/CryptoFunctionServiceTests.cs +++ b/test/Core.Test/Services/CryptoFunctionServiceTests.cs @@ -1,11 +1,11 @@ - + using System; -using System.Threading.Tasks; -using Xunit; -using Bit.Core.Services; -using Bit.Core.Enums; -using Bit.Test.Common.AutoFixture.Attributes; using System.Text; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; namespace Bit.Core.Test.Services { diff --git a/test/Core.Test/Services/SendServiceTests.cs b/test/Core.Test/Services/SendServiceTests.cs index fa2e3f94b..6038465d5 100644 --- a/test/Core.Test/Services/SendServiceTests.cs +++ b/test/Core.Test/Services/SendServiceTests.cs @@ -1,47 +1,44 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; using Bit.Core.Models.Response; +using Bit.Core.Models.View; using Bit.Core.Services; +using Bit.Core.Test.AutoFixture; using Bit.Core.Utilities; -using Bit.Core.Enums; using Bit.Test.Common; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Newtonsoft.Json; using NSubstitute; -using Xunit; -using System.Text; -using System.Net.Http; -using Bit.Core.Models.Request; -using Bit.Core.Test.AutoFixture; -using System.Linq.Expressions; -using Bit.Core.Models.View; -using Bit.Core.Exceptions; using NSubstitute.ExceptionExtensions; +using Xunit; namespace Bit.Core.Test.Services { public class SendServiceTests { - private string GetSendKey(string userId) => SendService.GetSendKey(userId); - [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task ReplaceAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); await sutProvider.Sut.ReplaceAsync(actualSendDataDict); - await sutProvider.GetDependency() - .Received(1).SaveAsync(GetSendKey(userId), actualSendDataDict); + await sutProvider.GetDependency().SetEncryptedSendsAsync(actualSendDataDict); } [Theory] @@ -53,9 +50,8 @@ namespace Bit.Core.Test.Services public async Task DeleteAsync_Success(int numberToDelete, SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency() - .GetAsync>(GetSendKey(userId)).Returns(actualSendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(actualSendDataDict); var idsToDelete = actualSendDataDict.Take(numberToDelete).Select(kvp => kvp.Key).ToArray(); var expectedSends = actualSendDataDict.Skip(numberToDelete).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -63,9 +59,8 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.DeleteAsync(idsToDelete); - await sutProvider.GetDependency().Received(1) - .SaveAsync(GetSendKey(userId), - Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory, SutAutoData] @@ -73,7 +68,7 @@ namespace Bit.Core.Test.Services { await sutProvider.Sut.ClearAsync(userId); - await sutProvider.GetDependency().Received(1).RemoveAsync(GetSendKey(userId)); + await sutProvider.GetDependency().SetEncryptedSendsAsync(null, userId); } [Theory] @@ -84,16 +79,15 @@ namespace Bit.Core.Test.Services var initialSendDatas = sendDatas.ToDictionary(d => d.Id, d => d); var idToDelete = initialSendDatas.First().Key; var expectedSends = initialSendDatas.Skip(1).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency() - .GetAsync>(Arg.Any()).Returns(initialSendDatas); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency() + .GetEncryptedSendsAsync(Arg.Any()).Returns(initialSendDatas); await sutProvider.Sut.DeleteWithServerAsync(idToDelete); await sutProvider.GetDependency().Received(1).DeleteSendAsync(idToDelete); - await sutProvider.GetDependency().Received(1) - .SaveAsync(GetSendKey(userId), - Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory] @@ -102,8 +96,8 @@ namespace Bit.Core.Test.Services public async Task GetAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); foreach (var dataKvp in sendDataDict) { @@ -114,11 +108,11 @@ namespace Bit.Core.Test.Services } [Theory, SutAutoData] - public async Task GetAsync_NonExistringId_ReturnsNull(SutProvider sutProvider, string userId, IEnumerable sendDatas) + public async Task GetAsync_NonExistingId_ReturnsNull(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAsync(Guid.NewGuid().ToString()); @@ -131,8 +125,8 @@ namespace Bit.Core.Test.Services public async Task GetAllAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var allExpected = sendDataDict.Select(kvp => new Send(kvp.Value)); var allActual = await sutProvider.Sut.GetAllAsync(); @@ -149,13 +143,13 @@ namespace Bit.Core.Test.Services { // TODO restore this once race condition is fixed or GHA can re-run jobs on individual platforms return; - + var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().HasKeyAsync().Returns(true); ServiceContainer.Register("cryptoService", sutProvider.GetDependency()); sutProvider.GetDependency().StringComparer.Returns(StringComparer.CurrentCulture); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAllDecryptedAsync(); @@ -175,7 +169,7 @@ namespace Bit.Core.Test.Services public async Task SaveWithServerAsync_NewTextSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { send.Id = null; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); @@ -208,7 +202,7 @@ namespace Bit.Core.Test.Services { send.Id = null; response.FileUploadType = FileUploadType.Azure; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); @@ -231,7 +225,7 @@ namespace Bit.Core.Test.Services public async Task SaveWithServerAsync_NewFileSend_LegacyFallback_Success(SutProvider sutProvider, string userId, Send send, SendResponse response) { send.Id = null; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); var error = new ErrorResponse(null, System.Net.HttpStatusCode.NotFound); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Throws(new ApiException(error)); sutProvider.GetDependency().PostSendFileAsync(Arg.Any()).Returns(response); @@ -248,7 +242,7 @@ namespace Bit.Core.Test.Services [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task SaveWithServerAsync_PutSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PutSendAsync(send.Id, Arg.Any()).Returns(response); await sutProvider.Sut.SaveWithServerAsync(send, null); @@ -272,7 +266,7 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.RemovePasswordWithServerAsync(sendId); await sutProvider.GetDependency().Received(1).PutSendRemovePasswordAsync(sendId); - await sutProvider.GetDependency().ReceivedWithAnyArgs(1).SaveAsync>(default, default); + await sutProvider.GetDependency().SetEncryptedSendsAsync(default, default); } [Theory] @@ -281,8 +275,8 @@ namespace Bit.Core.Test.Services public async Task UpsertAsync_Update_Success(SutProvider sutProvider, string userId, IEnumerable initialSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(initialSendDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var updatedSends = CoreHelpers.Clone(initialSendDict); foreach (var kvp in updatedSends) @@ -302,7 +296,8 @@ namespace Bit.Core.Test.Services } return true; }; - await sutProvider.GetDependency().Received(1).SaveAsync(GetSendKey(userId), Arg.Is>(d => matchSendsPredicate(d))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(d => matchSendsPredicate(d))); } [Theory] @@ -311,8 +306,8 @@ namespace Bit.Core.Test.Services public async Task UpsertAsync_NewSends_Success(SutProvider sutProvider, string userId, IEnumerable initialSends, IEnumerable newSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(initialSendDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var expectedDict = CoreHelpers.Clone(initialSendDict).Concat(newSends.Select(s => new KeyValuePair(s.Id, s))); @@ -328,7 +323,8 @@ namespace Bit.Core.Test.Services } return true; }; - await sutProvider.GetDependency().Received(1).SaveAsync(GetSendKey(userId), Arg.Is>(d => matchSendsPredicate(d))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(d => matchSendsPredicate(d))); } [Theory] diff --git a/test/Playground/Program.cs b/test/Playground/Program.cs index ca57eb5d2..c8e4708a7 100644 --- a/test/Playground/Program.cs +++ b/test/Playground/Program.cs @@ -1,6 +1,6 @@ -using Bit.Core.Services; -using System; +using System; using System.Threading.Tasks; +using Bit.Core.Services; namespace Bit.Playground {