Compare commits

..

1 Commits

Author SHA1 Message Date
André Bispo
1d05f5f758 [PM-6702] Add email verification feature flag to landing create account click 2024-08-16 14:46:31 +01:00
970 changed files with 11695 additions and 54588 deletions

5
.github/CODEOWNERS vendored
View File

@@ -5,10 +5,7 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default file owners.
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront
# Actions and workflow changes.
.github/workflows @bitwarden/dept-development-mobile
# * @bitwarden/tech-leads
# Auth
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev

View File

@@ -33,17 +33,17 @@ env:
jobs:
build:
name: Build
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Cache Gradle files
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
~/.gradle/caches
@@ -53,7 +53,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -62,13 +62,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
bundler-cache: true
@@ -84,29 +84,22 @@ jobs:
- name: Build
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
path: app/build/reports/tests/
publish_playstore:
name: Publish Play Store artifacts
needs:
- build
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
variant: ["prod", "dev"]
variant: ["prod", "qa"]
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Configure Ruby
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
bundler-cache: true
@@ -157,10 +150,10 @@ jobs:
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Cache Gradle files
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
~/.gradle/caches
@@ -170,7 +163,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -179,7 +172,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -237,14 +230,14 @@ jobs:
keyAlias:bitwarden-beta \
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
- name: Generate debug Play Store APKs
- name: Generate QA Play Store APKs
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
run: |
bundle exec fastlane assembleDebugApks
- name: Upload release Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
@@ -252,7 +245,7 @@ jobs:
- name: Upload beta Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
@@ -260,7 +253,7 @@ jobs:
- name: Upload release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
@@ -268,18 +261,18 @@ jobs:
- name: Upload beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
if-no-files-found: error
# When building variants other than 'prod'
- name: Upload debug .apk artifact
- name: Upload other .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
name: com.x8bit.bitwarden-${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
if-no-files-found: error
@@ -287,78 +280,78 @@ jobs:
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
> ./com.x8bit.bitwarden.apk-sha256.txt
> ./bw-android-apk-sha256.txt
- name: Create checksum for beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
> ./com.x8bit.bitwarden.beta.apk-sha256.txt
> ./bw-android-beta-apk-sha256.txt
- name: Create checksum for release .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
run: |
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
> ./com.x8bit.bitwarden.aab-sha256.txt
> ./bw-android-aab-sha256.txt
- name: Create checksum for beta .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
run: |
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
> ./bw-android-beta-aab-sha256.txt
- name: Create checksum for Debug .apk artifact
- name: Create checksum for other .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
> ./bw-android-${{ matrix.variant }}-apk-sha256.txt
- name: Upload .apk SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
name: bw-android-apk-sha256.txt
path: ./bw-android-apk-sha256.txt
if-no-files-found: error
- name: Upload .apk SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
name: bw-android-beta-apk-sha256.txt
path: ./bw-android-beta-apk-sha256.txt
if-no-files-found: error
- name: Upload .aab SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
name: bw-android-aab-sha256.txt
path: ./bw-android-aab-sha256.txt
if-no-files-found: error
- name: Upload .aab SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
name: bw-android-beta-aab-sha256.txt
path: ./bw-android-beta-aab-sha256.txt
if-no-files-found: error
- name: Upload .apk SHA file for debug
- name: Upload .apk SHA file for other
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release artifacts to Firebase
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
@@ -367,7 +360,7 @@ jobs:
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
- name: Publish beta artifacts to Firebase
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
@@ -382,21 +375,19 @@ jobs:
- name: Publish Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
run: |
bundle exec fastlane publishProdToPlayStore
bundle exec fastlane publishBetaToPlayStore
run: bundle exec fastlane publishBetaToPlayStore
publish_fdroid:
name: Publish F-Droid artifacts
needs:
- build
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Configure Ruby
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
bundler-cache: true
@@ -433,10 +424,10 @@ jobs:
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Cache Gradle files
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
~/.gradle/caches
@@ -446,7 +437,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -455,7 +446,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -478,6 +469,7 @@ jobs:
keyAlias:bitwarden \
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
# Generate the F-Droid APK for publishing
- name: Generate F-Droid Beta Artifacts
env:
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
@@ -490,7 +482,7 @@ jobs:
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
@@ -499,40 +491,40 @@ jobs:
- name: Create checksum for F-Droid artifact
run: |
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
> ./bw-fdroid-apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
name: bw-fdroid-apk-sha256.txt
path: ./bw-fdroid-apk-sha256.txt
if-no-files-found: error
- name: Upload F-Droid Beta .apk artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
name: com.x8bit.bitwarden-fdroid-beta.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
if-no-files-found: error
- name: Create checksum for F-Droid Beta artifact
run: |
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
> ./bw-fdroid-beta-apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
name: bw-fdroid-beta-apk-sha256.txt
path: ./bw-fdroid-beta-apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |

View File

@@ -1,3 +1,4 @@
---
name: Crowdin Sync
on:
@@ -9,12 +10,12 @@ on:
jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
env:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -29,7 +30,7 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -9,12 +9,12 @@ on:
jobs:
crowdin-push:
name: Crowdin Push
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
env:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
@@ -29,7 +29,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -9,8 +9,6 @@ on:
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
merge_group:
types: [checks_requested]
jobs:
check-run:
@@ -19,7 +17,7 @@ jobs:
sast:
name: SAST scan
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
@@ -28,12 +26,12 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -48,13 +46,13 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
@@ -62,13 +60,13 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@383f7e52eae3ab0510c3cb0e7d9d150bbaeab838 # v3.1.0
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,8 +8,6 @@ on:
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
merge_group:
type: [checks_requested]
workflow_dispatch:
env:
@@ -23,7 +21,7 @@ jobs:
test:
name: Test
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
@@ -33,15 +31,15 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Cache Gradle files
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
~/.gradle/caches
@@ -51,7 +49,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -60,12 +58,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -80,15 +78,8 @@ jobs:
run: |
bundle exec fastlane check
- name: Upload test reports on failure
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
path: app/build/reports/tests/
- name: Upload to codecov.io
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
file: app/build/reports/kover/reportStandardDebug.xml
env:

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.989.0)
aws-sdk-core (3.209.1)
aws-partitions (1.961.0)
aws-sdk-core (3.201.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-s3 (1.157.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0)
aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -39,8 +39,8 @@ GEM
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
excon (0.111.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -66,10 +66,10 @@ GEM
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.224.0)
fastlane (2.222.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -134,7 +134,7 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
@@ -155,12 +155,12 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.7)
http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.9.3)
jwt (2.8.2)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
@@ -179,7 +179,8 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.8)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -192,10 +193,11 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
time (0.4.0)
time (0.3.0)
date
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
@@ -203,15 +205,15 @@ GEM
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.25.1)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

View File

@@ -28,7 +28,6 @@
2. Create a `user.properties` file in the root directory of the project and add the following properties:
- `gitHubToken`: A "classic" Github Personal Access Token (PAT) with the `read:packages` scope (ex: `gitHubToken=gph_xx...xx`). These can be generated by going to the [Github tokens page](https://github.com/settings/tokens). See [the Github Packages user documentation concerning authentication](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details.
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing.bitwarden.com/getting-started/sdk/#linking-the-sdk-to-clients) for more details.
3. Setup the code style formatter:
@@ -171,11 +170,6 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: A networking layer interface.
- License: Apache 2.0
- **Timber**
- https://github.com/JakeWharton/timber
- Purpose: Extensible logging library for Android.
- License: Apache 2.0
- **zxcvbn4j**
- https://github.com/nulab/zxcvbn4j
- Purpose: Password strength estimation.

View File

@@ -2,8 +2,6 @@ import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFile
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
import com.google.gms.googleservices.GoogleServicesTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
@@ -22,16 +20,6 @@ plugins {
alias(libs.plugins.sonarqube)
}
/**
* Loads local user-specific build properties that are not checked into source control.
*/
val userProperties = Properties().apply {
val buildPropertiesFile = File(rootDir, "user.properties")
if (buildPropertiesFile.exists()) {
FileInputStream(buildPropertiesFile).use { load(it) }
}
}
android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
@@ -41,7 +29,7 @@ android {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "2024.9.0"
versionName = "2024.06.00"
setProperty("archivesBaseName", "com.x8bit.bitwarden")
@@ -73,9 +61,6 @@ android {
signingConfig = signingConfigs.getByName("debug")
isDebuggable = true
isMinifyEnabled = false
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "true")
}
// Beta and Release variants are identical except beta has a different package name
@@ -87,9 +72,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
}
release {
isDebuggable = false
@@ -98,9 +80,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
}
}
@@ -145,23 +124,11 @@ kotlin {
}
}
configurations.all {
resolutionStrategy.dependencySubstitution {
if ((userProperties["localSdk"] as String?).toBoolean()) {
substitute(module("com.bitwarden:sdk-android"))
.using(module("com.bitwarden:sdk-android:LOCAL"))
}
}
}
dependencies {
fun standardImplementation(dependencyNotation: Any) {
add("standardImplementation", dependencyNotation)
}
// TODO: this should use a versioned AAR instead of referencing a local AAR BITAU-94
implementation(files("libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.autofill)
@@ -203,7 +170,6 @@ dependencies {
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only
@@ -247,7 +213,6 @@ kover {
annotatedBy(
// Compose previews
"androidx.compose.ui.tooling.preview.Preview",
"androidx.compose.ui.tooling.preview.PreviewScreenSizes",
// Manually excluded classes/files/etc.
"com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage",
)
@@ -337,4 +302,4 @@ tasks {
getByName("sonar") {
dependsOn("check")
}
}
}

View File

@@ -6,10 +6,6 @@
# we keep it here.
-keep class com.bitwarden.** { *; }
# The Android Verifier component must be kept because it looks like dead code. Proguard is unable to
# see any JNI usage, so our rules must manually opt into keeping it.
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
################################################################################
# Bitwarden Models
################################################################################

View File

@@ -1,250 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f28200334a5c94feed1d9712e04ff01b')"
]
}
}

View File

@@ -1,250 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f28200334a5c94feed1d9712e04ff01b')"
]
}
}

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- For beta variant, we don't have a matching variant of the Bitwarden Authenticator app.
Therefore, we leave the known app cert null here so that no clients can connect to
AuthenticatorBridgeService in the beta variant. If later another variant of the
Bitwarden Authenticator app is added, a SHA-256 digest of that variant's APK can be added here.
-->
<string-array name="known_authenticator_app_certs" />
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="known_authenticator_app_certs">
<!-- This is the SHA-256 digest for the Authenticator App debug variant:-->
<item>13144ab52af797a88c2fe292674461ef1715e0e1e4f5f538f63f1c174696f476</item>
</string-array>
</resources>

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
/**
* CrashLogsManager implementation for F-droid flavor builds.
*/
class CrashLogsManagerImpl(
settingsRepository: SettingsRepository,
legacyAppCenterMigrator: LegacyAppCenterMigrator,
) : CrashLogsManager {
override var isEnabled: Boolean = true
override fun trackNonFatalException(e: Exception) = Unit
}

View File

@@ -1,27 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import timber.log.Timber
/**
* [LogsManager] implementation for F-droid flavor builds.
*/
class LogsManagerImpl(
settingsRepository: SettingsRepository,
legacyAppCenterMigrator: LegacyAppCenterMigrator,
) : LogsManager {
init {
if (BuildConfig.HAS_LOGS_ENABLED) {
Timber.plant(Timber.DebugTree())
}
}
override var isEnabled: Boolean = false
override fun setUserData(userId: String?, environmentType: Environment.Type) = Unit
override fun trackNonFatalException(throwable: Throwable) = Unit
}

View File

@@ -16,20 +16,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
Note that each build type uses a different value for knownCerts.
This in effect means that the only application that can connect to the debug/release/etc
variant AuthenticatorBridgeService is the debug/release/etc variant Bitwarden Authenticator
app. -->
<permission
android:name="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE"
android:knownCerts="@array/known_authenticator_app_certs"
android:label="Bitwarden Bridge"
android:protectionLevel="signature|knownSigner"
tools:targetApi="s" />
<application
android:name=".BitwardenApplication"
android:allowBackup="false"
@@ -76,42 +62,18 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="vault.bitwarden.com" />
<data android:host="vault.bitwarden.eu" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
<data android:host="bitwarden.com" />
<data android:host="bitwarden.pw" />
<data android:host="bitwarden.eu" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
<data android:host="totp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="bitwarden" />
</intent-filter>
</activity>
<activity
android:name=".AccessibilityActivity"
android:exported="false"
android:launchMode="singleTop"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".AutofillTotpCopyActivity"
android:exported="true"
@@ -196,26 +158,6 @@
</intent-filter>
</service>
<!--
The AccessibilityService name below refers to the legacy Xamarin app's service name. This
must always match in order for the app to properly query if it is providing accessibility
services.
-->
<!--suppress AndroidDomInspection -->
<service
android:name="com.x8bit.bitwarden.Accessibility.AccessibilityService"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service" />
</service>
<!--
The CredentialProviderService name below refers to the legacy Xamarin app's service name.
This must always match in order for the app to properly query if it is providing credential
@@ -247,79 +189,16 @@
android:value="true" />
</service>
<!--
The AutofillTileService name below refers to the legacy Xamarin app's service name.
This must always match in order for the app to properly query if it is providing autofill
tile services.
-->
<!--suppress AndroidDomInspection -->
<service
android:name="com.x8bit.bitwarden.AutofillTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/autofill"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!--
The GeneratorTileService name below refers to the legacy Xamarin app's service name.
This must always match in order for the app to properly query if it is providing generator
tile services.
-->
<!--suppress AndroidDomInspection -->
<service
android:name="com.x8bit.bitwarden.GeneratorTileService"
android:exported="true"
android:icon="@drawable/ic_generator"
android:label="@string/password_generator"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!--
The MyVaultTileService name below refers to the legacy Xamarin app's service name.
This must always match in order for the app to properly query if it is providing vault
tile services.
-->
<!--suppress AndroidDomInspection -->
<service
android:name="com.x8bit.bitwarden.MyVaultTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/my_vault"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<meta-data
android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<service
android:name="com.x8bit.bitwarden.data.platform.service.AuthenticatorBridgeService"
android:exported="true"
android:permission="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE" />
</application>
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
</queries>
</manifest>

View File

@@ -475,102 +475,7 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
},
{
"build": "release",
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
},
{
"build": "release",
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android.debug",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.naver.whale",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.fido.fido2client",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.heytap.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
},
{
"build": "release",
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
}
]
}
}
]
}

View File

@@ -1,80 +0,0 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.cromite.cromite",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_fdroid",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.fennec_dos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
},
{
"build": "release",
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.mulch",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.github.forkmaintainers.iceraven",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
}
]
}
}
]
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
* after the user clicks on the Autofill Quick Tile.
*/
@OmitFromCoverage
class AccessibilityActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finish()
}
}

View File

@@ -3,62 +3,37 @@ package com.x8bit.bitwarden
import android.app.Service
import android.content.Intent
import android.os.Build
import androidx.annotation.Keep
import androidx.core.app.AppComponentFactory
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService
private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService"
private const val LEGACY_CREDENTIAL_SERVICE_NAME =
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
/**
* A factory class that allows us to intercept when a manifest element is being instantiated
* and modify various characteristics before initialization.
*/
@Suppress("unused")
@Keep
@OmitFromCoverage
class BitwardenAppComponentFactory : AppComponentFactory() {
/**
* Used to intercept when certain legacy services are being instantiated and modify which
* service is created. This is required because the [className] used in the manifest must match
* the legacy Xamarin app service name but the service name in this app is different.
*
* Services currently being managed:
* * [BitwardenAccessibilityService]
* * [BitwardenAutofillService]
* * [BitwardenAutofillTileService]
* * [BitwardenFido2ProviderService]
* * [BitwardenVaultTileService]
* * [BitwardenGeneratorTileService]
* Used to intercept when the [BitwardenAutofillService] or [BitwardenFido2ProviderService] is
* being instantiated and modify which service is created. This is required because the
* [className] used in the manifest must match the legacy Xamarin app service name but the
* service name in this app is different.
*/
override fun instantiateServiceCompat(
cl: ClassLoader,
className: String,
intent: Intent?,
): Service = when (className) {
LEGACY_ACCESSIBILITY_SERVICE_NAME -> {
super.instantiateServiceCompat(
cl,
BitwardenAccessibilityService::class.java.name,
intent,
)
}
LEGACY_AUTOFILL_SERVICE_NAME -> {
super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent)
}
LEGACY_AUTOFILL_TILE_SERVICE_NAME -> {
super.instantiateServiceCompat(
cl,
BitwardenAutofillTileService::class.java.name,
intent,
)
}
LEGACY_CREDENTIAL_SERVICE_NAME -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
super.instantiateServiceCompat(
@@ -73,18 +48,6 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
}
}
LEGACY_VAULT_TILE_SERVICE_NAME -> {
super.instantiateServiceCompat(cl, BitwardenVaultTileService::class.java.name, intent)
}
LEGACY_GENERATOR_TILE_SERVICE_NAME -> {
super.instantiateServiceCompat(
cl,
BitwardenGeneratorTileService::class.java.name,
intent,
)
}
else -> super.instantiateServiceCompat(cl, className, intent)
}
}

View File

@@ -3,7 +3,7 @@ package com.x8bit.bitwarden
import android.app.Application
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
@@ -19,10 +19,10 @@ class BitwardenApplication : Application() {
// Inject classes here that must be triggered on startup but are not otherwise consumed by
// other callers.
@Inject
lateinit var logsManager: LogsManager
lateinit var networkConfigManager: NetworkConfigManager
@Inject
lateinit var networkConfigManager: NetworkConfigManager
lateinit var crashLogsManager: CrashLogsManager
@Inject
lateinit var authRequestNotificationManager: AuthRequestNotificationManager

View File

@@ -1,33 +0,0 @@
package com.x8bit.bitwarden
/**
* The legacy name for the accessibility service.
*/
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
/**
* The legacy name for the autofill service.
*/
const val LEGACY_AUTOFILL_SERVICE_NAME: String = "com.x8bit.bitwarden.Autofill.AutofillService"
/**
* The legacy name for the accessibility autofill tile service.
*/
const val LEGACY_AUTOFILL_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.AutofillTileService"
/**
* The legacy name for the credential service.
*/
const val LEGACY_CREDENTIAL_SERVICE_NAME: String =
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
/**
* The legacy name for the generator tile service.
*/
const val LEGACY_GENERATOR_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.GeneratorTileService"
/**
* The legacy name for the vault tile service.
*/
const val LEGACY_VAULT_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.MyVaultTileService"

View File

@@ -2,10 +2,7 @@ package com.x8bit.bitwarden
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -14,20 +11,17 @@ import androidx.compose.runtime.getValue
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
/**
@@ -39,29 +33,22 @@ class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
@Inject
lateinit var accessibilityActivityManager: AccessibilityActivityManager
@Inject
lateinit var autofillActivityManager: AutofillActivityManager
@Inject
lateinit var autofillCompletionManager: AutofillCompletionManager
@Inject
lateinit var accessibilityCompletionManager: AccessibilityCompletionManager
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
super.onCreate(savedInstanceState)
observeViewModelEvents()
if (savedInstanceState == null) {
mainViewModel.trySendAction(
MainAction.ReceiveFirstIntent(
@@ -79,33 +66,11 @@ class MainActivity : AppCompatActivity() {
}
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
EventsEffect(viewModel = mainViewModel) { event ->
when (event) {
is MainEvent.CompleteAccessibilityAutofill -> {
handleCompleteAccessibilityAutofill(event)
}
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.ShowToast -> {
Toast
.makeText(
baseContext,
event.message.invoke(resources),
Toast.LENGTH_SHORT,
)
.show()
}
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
navController = navController,
)
}
}
@@ -128,27 +93,16 @@ class MainActivity : AppCompatActivity() {
currentFocus?.clearFocus()
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
.takeIf { it }
?: super.dispatchTouchEvent(event)
override fun dispatchKeyEvent(event: KeyEvent): Boolean = debugLaunchManager
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
.takeIf { it }
?: super.dispatchKeyEvent(event)
private fun sendOpenDebugMenuEvent() {
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
}
private fun handleCompleteAccessibilityAutofill(
event: MainEvent.CompleteAccessibilityAutofill,
) {
accessibilityCompletionManager.completeAccessibilityAutofill(
activity = this,
cipherView = event.cipherView,
)
private fun observeViewModelEvents() {
mainViewModel
.eventFlow
.onEach { event ->
when (event) {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
}
}
.launchIn(lifecycleScope)
}
private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {

View File

@@ -5,12 +5,9 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
@@ -20,33 +17,23 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import javax.inject.Inject
@@ -59,9 +46,7 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val fido2CredentialManager: Fido2CredentialManager,
@@ -69,7 +54,6 @@ class MainViewModel @Inject constructor(
settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val savedStateHandle: SavedStateHandle,
private val clock: Clock,
) : BaseViewModel<MainState, MainEvent, MainAction>(
@@ -93,12 +77,6 @@ class MainViewModel @Inject constructor(
.onEach { specialCircumstance = it }
.launchIn(viewModelScope)
accessibilitySelectionManager
.accessibilitySelectionFlow
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
autofillSelectionManager
.autofillSelectionFlow
.onEach { trySendAction(MainAction.Internal.AutofillSelectionReceive(it)) }
@@ -148,27 +126,10 @@ class MainViewModel @Inject constructor(
}
}
.launchIn(viewModelScope)
// On app launch, mark all active users as having previously logged in.
// This covers any users who are active prior to this value being recorded.
viewModelScope.launch {
val userState = authRepository
.userStateFlow
.first()
userState
?.accounts
?.forEach {
settingsRepository.storeUserHasLoggedInValue(it.userId)
}
}
}
override fun handleAction(action: MainAction) {
when (action) {
is MainAction.Internal.AccessibilitySelectionReceive -> {
handleAccessibilitySelectionReceive(action)
}
is MainAction.Internal.AutofillSelectionReceive -> {
handleAutofillSelectionReceive(action)
}
@@ -179,25 +140,12 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
}
}
private fun handleOpenDebugMenu() {
sendEvent(MainEvent.NavigateToDebugMenu)
}
private fun handleAccessibilitySelectionReceive(
action: MainAction.Internal.AccessibilitySelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
}
private fun handleAutofillSelectionReceive(
action: MainAction.Internal.AutofillSelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
}
@@ -231,7 +179,7 @@ class MainViewModel @Inject constructor(
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Suppress("LongMethod")
private fun handleIntent(
intent: Intent,
isFirstIntent: Boolean,
@@ -240,39 +188,14 @@ class MainViewModel @Inject constructor(
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
val totpData: TotpData? =
// First grab TOTP URI directly from the intent data:
intent.getTotpDataOrNull()
?: run {
// Then check to see if the intent is coming from the Authenticator app:
if (intent.isAddTotpLoginItemFromAuthenticator()) {
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData.also {
// Clear pending add TOTP data so it is only handled once:
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = null
}
} else {
null
}
}
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
if (it != passwordlessRequestData.userId &&
!vaultRepository.isVaultUnlocked(it)
) {
// We only switch the account here if the current user's vault is not
// unlocked, otherwise prompt the user to allow us to change the account
// in the LoginApprovalScreen
authRepository.switchAccount(passwordlessRequestData.userId)
}
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = passwordlessRequestData,
@@ -283,7 +206,14 @@ class MainViewModel @Inject constructor(
}
completeRegistrationData != null -> {
handleCompleteRegistrationData(completeRegistrationData)
if (authRepository.activeUserId != null) {
authRepository.hasPendingAccountAddition = true
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.CompleteRegistration(
completeRegistrationData = completeRegistrationData,
timestamp = clock.millis(),
)
}
autofillSaveItem != null -> {
@@ -303,11 +233,6 @@ class MainViewModel @Inject constructor(
)
}
totpData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AddTotpLoginItem(data = totpData)
}
shareData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
@@ -358,11 +283,6 @@ class MainViewModel @Inject constructor(
hasVaultShortcut -> {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
}
hasAccountSecurityShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
}
}
@@ -370,49 +290,6 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
viewModelScope.launch {
// Attempt to load the environment for the user if they have a pre-auth environment
// saved.
environmentRepository.loadEnvironmentForEmail(userEmail = data.email)
// Determine if the token is still valid.
val emailTokenResult = authRepository.validateEmailToken(
email = data.email,
token = data.verificationToken,
)
when (emailTokenResult) {
is EmailTokenResult.Error -> {
sendEvent(
MainEvent.ShowToast(
message = emailTokenResult
.message
?.asText()
?: R.string.there_was_an_issue_validating_the_registration_token
.asText(),
),
)
}
EmailTokenResult.Expired -> {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance
.RegistrationEvent
.ExpiredRegistrationLink
}
EmailTokenResult.Success -> {
if (authRepository.activeUserId != null) {
authRepository.hasPendingAccountAddition = true
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
completeRegistrationData = data,
timestamp = clock.millis(),
)
}
}
}
}
}
/**
@@ -438,23 +315,10 @@ sealed class MainAction {
*/
data class ReceiveNewIntent(val intent: Intent) : MainAction()
/**
* Receive event to open the debug menu.
*/
data object OpenDebugMenu : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
sealed class Internal : MainAction() {
/**
* Indicates the user has manually selected the given [cipherView] for accessibility
* autofill.
*/
data class AccessibilitySelectionReceive(
val cipherView: CipherView,
) : Internal()
/**
* Indicates the user has manually selected the given [cipherView] for autofill.
*/
@@ -492,12 +356,6 @@ sealed class MainAction {
* Represents events that are emitted by the [MainViewModel].
*/
sealed class MainEvent {
/**
* Event indicating that the user has chosen the given [cipherView] for accessibility autofill
* and that the process is ready to complete.
*/
data class CompleteAccessibilityAutofill(val cipherView: CipherView) : MainEvent()
/**
* Event indicating that the user has chosen the given [cipherView] for autofill and that the
* process is ready to complete.
@@ -508,14 +366,4 @@ sealed class MainEvent {
* Event indicating that the UI should recreate itself.
*/
data object Recreate : MainEvent()
/**
* Navigate to the debug menu.
*/
data object NavigateToDebugMenu : MainEvent()
/**
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : MainEvent()
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@@ -12,13 +11,6 @@ import kotlinx.coroutines.flow.Flow
*/
@Suppress("TooManyFunctions")
interface AuthDiskSource {
/**
* The currently persisted authenticator sync symmetric key. This key is used for
* encrypting IPC traffic.
*/
var authenticatorSyncSymmetricKey: ByteArray?
/**
* Retrieves a unique ID for the application that is stored locally. This will generate a new
* one if it does not yet exist and it will only be reset for new installs or when clearing
@@ -53,43 +45,16 @@ interface AuthDiskSource {
*/
fun clearData(userId: String)
/**
* Get the authenticator sync unlock key. Null means there is no key, which means the user
* has not enabled authenticator syncing
*/
fun getAuthenticatorSyncUnlockKey(userId: String): String?
/**
* Store the authenticator sync unlock key. Storing a null key effectively disables
* authenticator syncing.
*/
fun storeAuthenticatorSyncUnlockKey(userId: String, authenticatorSyncUnlockKey: String?)
/**
* Retrieves the state indicating that the user should use a key connector.
*/
fun getShouldUseKeyConnector(userId: String): Boolean?
/**
* Retrieves the state indicating that the user should use a key connector as a flow.
*/
fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?>
/**
* Stores the boolean indicating that the user should use a key connector.
*/
fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?)
/**
* Retrieves the state indicating that the user has completed login with TDE.
*/
fun getIsTdeLoginComplete(userId: String): Boolean?
/**
* Stores the boolean indicating that the user has completed login with TDE.
*/
fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?)
/**
* Retrieves the state indicating that the user has chosen to trust this device.
*
@@ -290,35 +255,4 @@ interface AuthDiskSource {
* Stores the [accountTokens] for the given [userId].
*/
fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?)
/**
* Gets the onboarding status for the given [userId].
*/
fun getOnboardingStatus(userId: String): OnboardingStatus?
/**
* Stores the [onboardingStatus] for the given [userId].
*/
fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?)
/**
* Emits updates that track [getOnboardingStatus]. This will replay the last known value,
* if any exists.
*/
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
/**
* Gets the show import logins flag for the given [userId].
*/
fun getShowImportLogins(userId: String): Boolean?
/**
* Stores the show import logins flag for the given [userId].
*/
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
/**
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
@@ -19,8 +18,6 @@ import java.util.UUID
// These keys should be encrypted
private const val ACCOUNT_TOKENS_KEY = "accountTokens"
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric"
private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock"
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
private const val DEVICE_KEY_KEY = "deviceKey"
@@ -42,10 +39,7 @@ private const val TWO_FACTOR_TOKEN_KEY = "twoFactorToken"
private const val MASTER_PASSWORD_HASH_KEY = "keyHash"
private const val POLICIES_KEY = "policies"
private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
/**
* Primary implementation of [AuthDiskSource].
@@ -63,17 +57,12 @@ class AuthDiskSourceImpl(
AuthDiskSource {
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
private val mutableShouldUseKeyConnectorFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableOrganizationsFlowMap =
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
private val mutablePoliciesFlowMap =
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
private val mutableAccountTokensFlowMap =
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -96,14 +85,6 @@ class AuthDiskSourceImpl(
migrateAccountTokens()
}
override var authenticatorSyncSymmetricKey: ByteArray?
set(value) {
val asString = value?.let { value.toString(Charsets.ISO_8859_1) }
putEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY, asString)
}
get() = getEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY)
?.toByteArray(Charsets.ISO_8859_1)
override val uniqueAppId: String
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
@@ -143,32 +124,11 @@ class AuthDiskSourceImpl(
storePolicies(userId = userId, policies = null)
storeAccountTokens(userId = userId, accountTokens = null)
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
// Do not remove OnboardingStatus we want to keep track of this even after logout.
}
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
getEncryptedString(AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId))
override fun storeAuthenticatorSyncUnlockKey(
userId: String,
authenticatorSyncUnlockKey: String?,
) {
putEncryptedString(
key = AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId),
value = authenticatorSyncUnlockKey,
)
}
override fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?> =
getMutableShouldUseKeyConnectorFlowMap(userId = userId)
.onSubscription { emit(getShouldUseKeyConnector(userId = userId)) }
override fun getShouldUseKeyConnector(
userId: String,
): Boolean? = getBoolean(key = USES_KEY_CONNECTOR.appendIdentifier(userId))
@@ -178,15 +138,6 @@ class AuthDiskSourceImpl(
key = USES_KEY_CONNECTOR.appendIdentifier(userId),
value = shouldUseKeyConnector,
)
getMutableShouldUseKeyConnectorFlowMap(userId = userId).tryEmit(shouldUseKeyConnector)
}
override fun getIsTdeLoginComplete(
userId: String,
): Boolean? = getBoolean(key = TDE_LOGIN_COMPLETE.appendIdentifier(userId))
override fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?) {
putBoolean(TDE_LOGIN_COMPLETE.appendIdentifier(userId), isTdeLoginComplete)
}
override fun getShouldTrustDevice(
@@ -422,41 +373,6 @@ class AuthDiskSourceImpl(
getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens)
}
override fun getOnboardingStatus(userId: String): OnboardingStatus? {
return getString(key = ONBOARDING_STATUS_KEY.appendIdentifier(userId))?.let {
json.decodeFromStringOrNull(it)
}
}
override fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) {
putString(
key = ONBOARDING_STATUS_KEY.appendIdentifier(userId),
value = onboardingStatus?.let { json.encodeToString(it) },
)
getMutableOnboardingStatusFlow(userId = userId).tryEmit(onboardingStatus)
}
override fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?> {
return getMutableOnboardingStatusFlow(userId = userId)
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
}
override fun getShowImportLogins(userId: String): Boolean? {
return getBoolean(SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId))
}
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
putBoolean(
key = SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId),
value = showImportLogins,
)
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
}
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
@@ -465,20 +381,6 @@ class AuthDiskSourceImpl(
putString(key = UNIQUE_APP_ID_KEY, value = it)
}
private fun getMutableOnboardingStatusFlow(
userId: String,
): MutableSharedFlow<OnboardingStatus?> =
mutableOnboardingStatusFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShouldUseKeyConnectorFlowMap(
userId: String,
): MutableSharedFlow<Boolean?> =
mutableShouldUseKeyConnectorFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableOrganizationsFlow(
userId: String,
): MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?> =
@@ -500,12 +402,6 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -37,7 +37,6 @@ data class EnvironmentUrlDataJson(
@SerialName("events")
val events: String? = null,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Default [EnvironmentUrlDataJson] for the US region.

View File

@@ -1,41 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Describes the current status of a user in the account onboarding steps.
*/
@Serializable
enum class OnboardingStatus {
/**
* Onboarding has not yet started.
*/
@SerialName("notStarted")
NOT_STARTED,
/**
* The user is completing the account lock setup.
*/
@SerialName("accountLockSetup")
ACCOUNT_LOCK_SETUP,
/**
* The user is completing the auto fill service setup.
*/
@SerialName("autofillSetup")
AUTOFILL_SETUP,
/**
* The user is completing the final step of the onboarding process.
*/
@SerialName("finalStep")
FINAL_STEP,
/**
* The user has completed all onboarding steps.
*/
@SerialName("complete")
COMPLETE,
}

View File

@@ -1,17 +1,14 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
/**
* Defines raw calls under the /accounts API.
*/
interface UnauthenticatedAccountsApi {
interface AccountsApi {
@POST("/accounts/password-hint")
suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson,
@@ -21,10 +18,4 @@ interface UnauthenticatedAccountsApi {
suspend fun resendVerificationCodeEmail(
@Body body: ResendEmailRequestJson,
): Result<Unit>
@POST("/accounts/set-key-connector-key")
suspend fun setKeyConnectorKey(
@Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<Unit>
}

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
@@ -52,6 +53,12 @@ interface AuthenticatedAccountsApi {
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
/**
* Sets the key connector key.
*/
@POST("/accounts/set-key-connector-key")
suspend fun setKeyConnectorKey(@Body body: KeyConnectorKeyRequestJson): Result<Unit>
/**
* Sets the password.
*/

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
/**
* Defines raw calls specific for key connectors that use custom urls.
*/
@Keep
interface AuthenticatedKeyConnectorApi {
@POST
suspend fun storeMasterKeyToKeyConnector(
@Url url: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit>
}

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import kotlinx.serialization.json.JsonPrimitive
import retrofit2.Call
import retrofit2.http.Body
@@ -23,7 +22,7 @@ import retrofit2.http.Query
/**
* Defines raw calls under the /identity API.
*/
interface UnauthenticatedIdentityApi {
interface IdentityApi {
@POST("/connect/token")
@Suppress("LongParameterList")
@@ -80,9 +79,4 @@ interface UnauthenticatedIdentityApi {
suspend fun sendVerificationEmail(
@Body body: SendVerificationEmailRequestJson,
): Result<JsonPrimitive?>
@POST("/accounts/register/verification-email-clicked")
suspend fun verifyEmailToken(
@Body body: VerifyEmailTokenRequestJson,
): Result<Unit>
}

View File

@@ -8,7 +8,7 @@ import retrofit2.http.POST
/**
* Defines raw calls under the /organizations API.
*/
interface UnauthenticatedOrganizationApi {
interface OrganizationApi {
/**
* Checks for the claimed domain organization of an email for SSO purposes.
*/

View File

@@ -1,30 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
/**
* Defines raw calls specific for key connectors that use custom urls.
*/
@Keep
interface UnauthenticatedKeyConnectorApi {
@POST
suspend fun storeMasterKeyToKeyConnector(
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit>
@GET
suspend fun getMasterKeyFromKeyConnector(
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<KeyConnectorMasterKeyResponseJson>
}

View File

@@ -36,12 +36,8 @@ object AuthNetworkModule {
retrofits: Retrofits,
json: Json,
): AccountsService = AccountsServiceImpl(
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
authenticatedKeyConnectorApi = retrofits
.createStaticRetrofit(isAuthenticated = true)
.create(),
json = json,
)
@@ -68,7 +64,7 @@ object AuthNetworkModule {
retrofits: Retrofits,
json: Json,
): IdentityService = IdentityServiceImpl(
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
api = retrofits.unauthenticatedIdentityRetrofit.create(),
json = json,
)
@@ -97,6 +93,6 @@ object AuthNetworkModule {
retrofits: Retrofits,
): OrganizationService = OrganizationServiceImpl(
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
organizationApi = retrofits.unauthenticatedApiRetrofit.create(),
)
}

View File

@@ -28,7 +28,6 @@ sealed class GetTokenResponseJson {
* this token will be cached and used for future auth requests.
* @property masterPasswordPolicyOptions The options available for a user's master password.
* @property userDecryptionOptions The options available to a user for decryption.
* @property keyConnectorUrl URL to the user's key connector.
*/
@Serializable
data class Success(
@@ -76,9 +75,6 @@ sealed class GetTokenResponseJson {
@SerialName("UserDecryptionOptions")
val userDecryptionOptions: UserDecryptionOptionsJson?,
@SerialName("KeyConnectorUrl")
val keyConnectorUrl: String?,
) : GetTokenResponseJson()
/**
@@ -96,17 +92,9 @@ sealed class GetTokenResponseJson {
@Serializable
data class Invalid(
@SerialName("ErrorModel")
val errorModel: ErrorModel?,
@SerialName("errorModel")
val legacyErrorModel: LegacyErrorModel?,
val errorModel: ErrorModel,
) : GetTokenResponseJson() {
/**
* The error message returned from the server, or null.
*/
val errorMessage: String?
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
/**
* The error body of an invalid request containing a message.
*/
@@ -115,18 +103,6 @@ sealed class GetTokenResponseJson {
@SerialName("Message")
val errorMessage: String,
)
/**
* The legacy error body of an invalid request containing a message.
*
* This model is used to support older versions of the error response model that used
* lower-case keys.
*/
@Serializable
data class LegacyErrorModel(
@SerialName("message")
val errorMessage: String,
)
}
/**

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents the request body used to store the master key in the cloud.
*/
@Serializable
data class KeyConnectorMasterKeyRequestJson(
@SerialName("Key") val masterKey: String,
)

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents the response body used to retrieve the master key from the cloud.
*/
@Serializable
data class KeyConnectorMasterKeyResponseJson(
@SerialName("key") val masterKey: String,
)

View File

@@ -1,16 +1,25 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Response object returned when requesting organization domain SSO details.
*
* @property isSsoAvailable Whether or not SSO is available for this domain.
* @property domainName The organization's domain name.
* @property organizationIdentifier The organization's identifier.
* @property isSsoRequired Whether or not SSO is required.
* @property verifiedDate The date these details were verified.
*/
@Serializable
data class OrganizationDomainSsoDetailsResponseJson(
@SerialName("ssoAvailable") val isSsoAvailable: Boolean,
@SerialName("domainName") val domainName: String,
@SerialName("organizationIdentifier") val organizationIdentifier: String,
@SerialName("ssoRequired") val isSsoRequired: Boolean,
@Contextual
@SerialName("verifiedDate") val verifiedDate: ZonedDateTime?,
)

View File

@@ -46,6 +46,7 @@ sealed class RegisterResponseJson {
/**
* Represents the json body of an invalid register request.
*
* @param message
* @param validationErrors a map where each value is a list of error messages for each key.
* The values in the array should be used for display to the user, since the keys tend to come
* back as nonsense. (eg: empty string key)
@@ -53,17 +54,18 @@ sealed class RegisterResponseJson {
@Serializable
data class Invalid(
@SerialName("message")
private val invalidMessage: String? = null,
@SerialName("Message")
private val errorMessage: String? = null,
val message: String?,
@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : RegisterResponseJson() {
/**
* A generic error message.
*/
val message: String? get() = invalidMessage ?: errorMessage
}
) : RegisterResponseJson()
/**
* A different register error with a message.
*/
@Serializable
data class Error(
@SerialName("Message")
val message: String?,
) : RegisterResponseJson()
}

View File

@@ -22,6 +22,7 @@ sealed class SendVerificationEmailResponseJson {
/**
* Represents the json body of an invalid request.
*
* @param message
* @param validationErrors a map where each value is a list of error messages for each key.
* The values in the array should be used for display to the user, since the keys tend to come
* back as nonsense. (eg: empty string key)
@@ -29,17 +30,18 @@ sealed class SendVerificationEmailResponseJson {
@Serializable
data class Invalid(
@SerialName("message")
private val invalidMessage: String? = null,
@SerialName("Message")
private val errorMessage: String? = null,
val message: String?,
@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : SendVerificationEmailResponseJson() {
/**
* A generic error message.
*/
val message: String? get() = invalidMessage ?: errorMessage
}
) : SendVerificationEmailResponseJson()
/**
* A different error with a message.
*/
@Serializable
data class Error(
@SerialName("Message")
val message: String?,
) : SendVerificationEmailResponseJson()
}

View File

@@ -1,18 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models the request body for verify email token endpoint.
*
* @param email the email address of the user to verify.
* @param token the provided email verification token.
*/
@Serializable
data class VerifyEmailTokenRequestJson(
@SerialName("email")
val email: String,
@SerialName("emailVerificationToken")
val token: String,
)

View File

@@ -1,38 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Model the response of a verify email token request.
*
* A valid response will be a [VerifyEmailTokenResponseJson.Valid]
*
* an invalid response will be a [VerifyEmailTokenResponseJson.Invalid] with a message.
*/
@Serializable
sealed class VerifyEmailTokenResponseJson {
/**
* The token is confirmed as valid from the response.
*/
@Serializable
data object Valid : VerifyEmailTokenResponseJson()
/**
* The response is invalid.
*
* @property message The error message. Expected to explain the reason why the token is invalid.
*/
@Serializable
data class Invalid(
@SerialName("message")
val message: String,
) : VerifyEmailTokenResponseJson()
/**
* The token has expired. This is special case of similar to [Invalid].
*/
@Serializable
data object TokenExpired : VerifyEmailTokenResponseJson()
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
@@ -11,7 +10,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
/**
* Provides an API for querying accounts endpoints.
*/
@Suppress("TooManyFunctions")
interface AccountsService {
/**
@@ -59,48 +57,11 @@ interface AccountsService {
/**
* Set the key connector key.
*
* This API requires the [accessToken] to be passed in manually because it occurs during the
* login process.
*/
suspend fun setKeyConnectorKey(
accessToken: String,
body: KeyConnectorKeyRequestJson,
): Result<Unit>
suspend fun setKeyConnectorKey(body: KeyConnectorKeyRequestJson): Result<Unit>
/**
* Set the password.
*/
suspend fun setPassword(body: SetPasswordRequestJson): Result<Unit>
/**
* Retrieves the master key from the key connector.
*
* This API requires the [accessToken] to be passed in manually because it occurs during the
* login process.
*/
suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson>
/**
* Stores the master key to the key connector.
*/
suspend fun storeMasterKeyToKeyConnector(
url: String,
masterKey: String,
): Result<Unit>
/**
* Stores the master key to the key connector.
*
* This API requires the [accessToken] to be passed in manually because it occurs during the
* login process.
*/
suspend fun storeMasterKeyToKeyConnector(
url: String,
accessToken: String,
masterKey: String,
): Result<Unit>
}

View File

@@ -1,15 +1,11 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedKeyConnectorApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedKeyConnectorApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
@@ -17,19 +13,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordReque
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import kotlinx.serialization.json.Json
/**
* The default implementation of the [AccountsService].
*/
@Suppress("TooManyFunctions")
class AccountsServiceImpl(
private val unauthenticatedAccountsApi: UnauthenticatedAccountsApi,
private val accountsApi: AccountsApi,
private val authenticatedAccountsApi: AuthenticatedAccountsApi,
private val unauthenticatedKeyConnectorApi: UnauthenticatedKeyConnectorApi,
private val authenticatedKeyConnectorApi: AuthenticatedKeyConnectorApi,
private val json: Json,
) : AccountsService {
@@ -87,7 +76,7 @@ class AccountsServiceImpl(
override suspend fun requestPasswordHint(
email: String,
): Result<PasswordHintResponseJson> =
unauthenticatedAccountsApi
accountsApi
.passwordHintRequest(PasswordHintRequestJson(email))
.map { PasswordHintResponseJson.Success }
.recoverCatching { throwable ->
@@ -101,7 +90,7 @@ class AccountsServiceImpl(
}
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
accountsApi.resendVerificationCodeEmail(body = body)
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
return if (body.currentPasswordHash == null) {
@@ -112,43 +101,10 @@ class AccountsServiceImpl(
}
override suspend fun setKeyConnectorKey(
accessToken: String,
body: KeyConnectorKeyRequestJson,
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
): Result<Unit> = authenticatedAccountsApi.setKeyConnectorKey(body)
override suspend fun setPassword(
body: SetPasswordRequestJson,
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
override suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson> =
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
override suspend fun storeMasterKeyToKeyConnector(
url: String,
masterKey: String,
): Result<Unit> =
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
override suspend fun storeMasterKeyToKeyConnector(
url: String,
accessToken: String,
masterKey: String,
): Result<Unit> =
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
}

View File

@@ -10,8 +10,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
/**
* Provides an API for querying identity endpoints.
@@ -74,12 +72,4 @@ interface IdentityService {
* Register a new account to Bitwarden using email verification flow.
*/
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
/**
* Makes request to verify email registration token. If the token provided is
* still valid will return success.
*/
suspend fun verifyEmailRegistrationToken(
body: VerifyEmailTokenRequestJson,
): Result<VerifyEmailTokenResponseJson>
}

View File

@@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedIdentityApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
@@ -12,8 +12,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
@@ -22,17 +20,17 @@ import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
import kotlinx.serialization.json.Json
class IdentityServiceImpl(
private val unauthenticatedIdentityApi: UnauthenticatedIdentityApi,
private val api: IdentityApi,
private val json: Json,
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
) : IdentityService {
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
api.preLogin(PreLoginRequestJson(email = email))
@Suppress("MagicNumber")
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
api
.register(body)
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
@@ -41,8 +39,13 @@ class IdentityServiceImpl(
code = 400,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf(400, 429),
?: bitwardenError
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf(400, 429),
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
code = 429,
json = json,
)
?: throw throwable
@@ -55,7 +58,7 @@ class IdentityServiceImpl(
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel?,
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
): Result<GetTokenResponseJson> = api
.getToken(
scope = "api offline_access",
clientId = "mobile",
@@ -91,14 +94,14 @@ class IdentityServiceImpl(
override suspend fun prevalidateSso(
organizationIdentifier: String,
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
): Result<PrevalidateSsoResponseJson> = api
.prevalidateSso(
organizationIdentifier = organizationIdentifier,
)
override fun refreshTokenSynchronously(
refreshToken: String,
): Result<RefreshTokenResponseJson> = unauthenticatedIdentityApi
): Result<RefreshTokenResponseJson> = api
.refreshTokenCall(
clientId = "mobile",
grantType = "refresh_token",
@@ -110,7 +113,7 @@ class IdentityServiceImpl(
override suspend fun registerFinish(
body: RegisterFinishRequestJson,
): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
api
.registerFinish(body)
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
@@ -119,45 +122,18 @@ class IdentityServiceImpl(
codes = listOf(400, 429),
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
code = 429,
json = json,
)
?: throw throwable
}
override suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<String?> {
return unauthenticatedIdentityApi
return api
.sendVerificationEmail(body = body)
.map { it?.content }
}
override suspend fun verifyEmailRegistrationToken(
body: VerifyEmailTokenRequestJson,
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi
.verifyEmailToken(
body = body,
)
.map {
VerifyEmailTokenResponseJson.Valid
}
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
code = 400,
json = json,
)
?.checkForExpiredMessage()
?: throw throwable
}
}
/**
* If the message body contains text related to the token being expired, return
* the TokenExpired type. Otherwise, return the original Invalid response.
*/
private fun VerifyEmailTokenResponseJson.Invalid.checkForExpiredMessage() =
if (message.contains(other = "expired", ignoreCase = true)) {
VerifyEmailTokenResponseJson.TokenExpired
} else {
this
}

View File

@@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedOrganizationApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedOrganizationApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.OrganizationApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
@@ -13,7 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetP
*/
class OrganizationServiceImpl(
private val authenticatedOrganizationApi: AuthenticatedOrganizationApi,
private val unauthenticatedOrganizationApi: UnauthenticatedOrganizationApi,
private val organizationApi: OrganizationApi,
) : OrganizationService {
override suspend fun organizationResetPasswordEnroll(
organizationId: String,
@@ -32,7 +32,7 @@ class OrganizationServiceImpl(
override suspend fun getOrganizationDomainSsoDetails(
email: String,
): Result<OrganizationDomainSsoDetailsResponseJson> = unauthenticatedOrganizationApi
): Result<OrganizationDomainSsoDetailsResponseJson> = organizationApi
.getClaimedDomainOrganizationDetails(
body = OrganizationDomainSsoDetailsRequestJson(
email = email,

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk
import com.bitwarden.core.AuthRequestResponse
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.core.RegisterTdeKeyResponse
@@ -38,11 +37,6 @@ interface AuthSdkSource {
purpose: HashPurpose,
): Result<String>
/**
* Creates a set of encryption key information for use with a key connector.
*/
suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse>
/**
* Creates a set of encryption key information for registration.
*/

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.sdk
import com.bitwarden.core.AuthRequestResponse
import com.bitwarden.core.FingerprintRequest
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.core.RegisterTdeKeyResponse
@@ -64,13 +63,6 @@ class AuthSdkSourceImpl(
)
}
override suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse> =
runCatchingWithLogs {
getClient()
.auth()
.makeKeyConnectorKeys()
}
override suspend fun makeRegisterKeys(
email: String,
password: String,

View File

@@ -1,15 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData
/**
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
* item.
*/
interface AddTotpItemFromAuthenticatorManager {
/**
* Current pending [TotpData] to be added from the Authenticator app.
*/
var pendingAddTotpLoginItemData: TotpData?
}

View File

@@ -1,11 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData
/**
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
*/
class AddTotpItemFromAuthenticatorManagerImpl : AddTotpItemFromAuthenticatorManager {
override var pendingAddTotpLoginItemData: TotpData? = null
}

View File

@@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import kotlinx.coroutines.flow.Flow
/**
* A manager class for handling authentication for logging in with remote device.
* A manager class for handling authentication fo logging in with remote device.
*/
interface AuthRequestManager {
/**

View File

@@ -1,46 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
/**
* Manager used to interface with a key connector.
*/
interface KeyConnectorManager {
/**
* Retrieves the master key from the key connector.
*/
suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson>
/**
* Migrates an existing user to use the key connector.
*/
@Suppress("LongParameterList")
suspend fun migrateExistingUserToKeyConnector(
userId: String,
url: String,
userKeyEncrypted: String,
email: String,
masterPassword: String,
kdf: Kdf,
): Result<Unit>
/**
* Migrates a new user to use the key connector.
*/
@Suppress("LongParameterList")
suspend fun migrateNewUserToKeyConnector(
url: String,
accessToken: String,
kdfType: KdfTypeJson,
kdfIterations: Int?,
kdfMemory: Int?,
kdfParallelism: Int?,
organizationIdentifier: String,
): Result<KeyConnectorResponse>
}

View File

@@ -1,88 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
/**
* The default implementation of the [KeyConnectorManager].
*/
class KeyConnectorManagerImpl(
private val accountsService: AccountsService,
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
) : KeyConnectorManager {
override suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson> =
accountsService.getMasterKeyFromKeyConnector(
url = url,
accessToken = accessToken,
)
override suspend fun migrateExistingUserToKeyConnector(
userId: String,
url: String,
userKeyEncrypted: String,
email: String,
masterPassword: String,
kdf: Kdf,
): Result<Unit> =
vaultSdkSource
.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
password = masterPassword,
kdf = kdf,
)
.flatMap { masterKey ->
accountsService.storeMasterKeyToKeyConnector(url = url, masterKey = masterKey)
}
.flatMap { accountsService.convertToKeyConnector() }
override suspend fun migrateNewUserToKeyConnector(
url: String,
accessToken: String,
kdfType: KdfTypeJson,
kdfIterations: Int?,
kdfMemory: Int?,
kdfParallelism: Int?,
organizationIdentifier: String,
): Result<KeyConnectorResponse> =
authSdkSource
.makeKeyConnectorKeys()
.flatMap { keyConnectorResponse ->
accountsService
.storeMasterKeyToKeyConnector(
url = url,
accessToken = accessToken,
masterKey = keyConnectorResponse.masterKey,
)
.flatMap {
accountsService.setKeyConnectorKey(
accessToken = accessToken,
body = KeyConnectorKeyRequestJson(
userKey = keyConnectorResponse.encryptedUserKey,
keys = KeyConnectorKeyRequestJson.Keys(
publicKey = keyConnectorResponse.keys.public,
encryptedPrivateKey = keyConnectorResponse.keys.private,
),
kdfType = kdfType,
kdfIterations = kdfIterations,
kdfMemory = kdfMemory,
kdfParallelism = kdfParallelism,
organizationIdentifier = organizationIdentifier,
),
)
}
.map { keyConnectorResponse }
}
}

View File

@@ -18,7 +18,6 @@ class TrustedDeviceManagerImpl(
) : TrustedDeviceManager {
override suspend fun trustThisDeviceIfNecessary(userId: String): Result<Boolean> =
if (authDiskSource.getShouldTrustDevice(userId = userId) != true) {
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
false.asSuccess()
} else {
vaultSdkSource
@@ -52,7 +51,6 @@ class TrustedDeviceManagerImpl(
userId = userId,
previousUserState = requireNotNull(authDiskSource.userState),
)
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
}
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
.map { Unit }

View File

@@ -14,15 +14,15 @@ interface UserLogoutManager {
val logoutEventFlow: SharedFlow<LogoutEvent>
/**
* Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
* will be displayed letting the user know the session has expired.
* Completely logs out the given [userId], removing all data.
* If [isExpired] is true, a toast will be displayed
* letting the user know the session has expired.
*/
fun logout(userId: String, isExpired: Boolean = false)
/**
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
* the exception of basic account data. If [isExpired] is true, a toast will be displayed
* letting the user know the session has expired.
* the exception of basic account data.
*/
fun softLogout(userId: String, isExpired: Boolean = false)
fun softLogout(userId: String)
}

View File

@@ -64,10 +64,7 @@ class UserLogoutManagerImpl(
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
}
override fun softLogout(userId: String, isExpired: Boolean) {
if (isExpired) {
showToast(message = R.string.login_expired)
}
override fun softLogout(userId: String) {
authDiskSource.storeAccountTokens(
userId = userId,
accountTokens = null,
@@ -77,11 +74,7 @@ class UserLogoutManagerImpl(
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
switchUserIfAvailable(
currentUserId = userId,
removeCurrentUserFromAccounts = false,
isExpired = isExpired,
)
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false)
clearData(userId = userId)
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))

View File

@@ -2,19 +2,14 @@ package com.x8bit.bitwarden.data.auth.manager.di
import android.content.Context
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManagerImpl
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
@@ -76,19 +71,6 @@ object AuthManagerModule {
authDiskSource = authDiskSource,
)
@Provides
@Singleton
fun provideKeyConnectorManager(
accountsService: AccountsService,
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
): KeyConnectorManager =
KeyConnectorManagerImpl(
accountsService = accountsService,
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
)
@Provides
@Singleton
fun provideTrustedDeviceManager(
@@ -126,9 +108,4 @@ object AuthManagerModule {
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
AddTotpItemFromAuthenticatorManagerImpl()
}

View File

@@ -11,7 +11,7 @@ val AuthRequestType.isSso: Boolean
AuthRequestType.OTHER_DEVICE -> false
AuthRequestType.SSO_OTHER_DEVICE,
AuthRequestType.SSO_ADMIN_APPROVAL,
-> true
-> true
}
/**
@@ -21,7 +21,7 @@ fun AuthRequestType.toAuthRequestTypeJson(): AuthRequestTypeJson =
when (this) {
AuthRequestType.OTHER_DEVICE,
AuthRequestType.SSO_OTHER_DEVICE,
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
AuthRequestType.SSO_ADMIN_APPROVAL -> AuthRequestTypeJson.ADMIN_APPROVAL
}

View File

@@ -1,14 +1,12 @@
package com.x8bit.bitwarden.data.auth.repository
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
@@ -18,7 +16,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
@@ -105,11 +102,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
var rememberedOrgIdentifier: String?
/**
* The currently persisted state indicating whether the user has completed login via TDE.
*/
val tdeLoginComplete: Boolean?
/**
* The currently persisted state indicating whether the user has trusted this device.
*/
@@ -212,7 +204,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**
@@ -276,12 +267,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
): PasswordHintResult
/**
* Removes the users password from the account. This used used when migrating from master
* password login to key connector login.
*/
suspend fun removePassword(masterPassword: String): RemovePasswordResult
/**
* Resets the users password from the [currentPassword] (or null for account recovery resets),
* to the [newPassword] and optional [passwordHint].
@@ -380,22 +365,4 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
name: String,
receiveMarketingEmails: Boolean,
): SendVerificationEmailResult
/**
* Validates the given [token] for the given [email]. Part of th new account registration flow.
*/
suspend fun validateEmailToken(
email: String,
token: String,
): EmailTokenResult
/**
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
/**
* Update the value of the showImportLogins status for the user.
*/
fun setShowImportLogins(showImportLogins: Boolean)
}

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
@@ -27,8 +26,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
@@ -38,13 +35,11 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
@@ -54,7 +49,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
@@ -62,7 +56,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResul
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
@@ -75,41 +68,30 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@@ -156,18 +138,14 @@ class AuthRepositoryImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val configDiskSource: ConfigDiskSource,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRequestManager: AuthRequestManager,
private val keyConnectorManager: KeyConnectorManager,
private val trustedDeviceManager: TrustedDeviceManager,
private val userLogoutManager: UserLogoutManager,
private val policyManager: PolicyManager,
private val featureFlagManager: FeatureFlagManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
pushManager: PushManager,
dispatcherManager: DispatcherManager,
) : AuthRepository,
@@ -260,9 +238,6 @@ class AuthRepositoryImpl(
authDiskSource.userStateFlow,
authDiskSource.userAccountTokensFlow,
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultRepository.vaultUnlockDataStateFlow,
mutableHasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
@@ -274,22 +249,16 @@ class AuthRepositoryImpl(
val userStateJson = array[0] as UserStateJson?
val userAccountTokens = array[1] as List<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
val vaultState = array[3] as List<VaultUnlockData>
val hasPendingAccountAddition = array[4] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
userOrganizationsList = userOrganizationsList,
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
hasPendingAccountAddition = hasPendingAccountAddition,
onboardingStatus = onboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
@@ -303,13 +272,10 @@ class AuthRepositoryImpl(
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
userAccountTokens = authDiskSource.userAccountTokens,
userOrganizationsList = authDiskSource.userOrganizationsList,
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
onboardingStatus = authDiskSource.currentOnboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
@@ -334,9 +300,6 @@ class AuthRepositoryImpl(
override var rememberedOrgIdentifier: String? by authDiskSource::rememberedOrgIdentifier
override val tdeLoginComplete: Boolean?
get() = activeUserId?.let { authDiskSource.getIsTdeLoginComplete(userId = it) }
override var shouldTrustDevice: Boolean
get() = activeUserId?.let { authDiskSource.getShouldTrustDevice(userId = it) } ?: false
set(value) {
@@ -366,24 +329,6 @@ class AuthRepositoryImpl(
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
init {
combine(
mutableHasPendingAccountAdditionStateFlow,
authDiskSource.userStateFlow,
environmentRepository.environmentStateFlow,
) { hasPendingAddition, userState, environment ->
logsManager.setUserData(
userId = userState?.activeUserId.takeUnless { hasPendingAddition },
environmentType = userState
?.activeAccount
?.settings
?.environmentUrlData
?.toEnvironmentUrls()
?.type
.takeUnless { hasPendingAddition }
?: environment.type,
)
}
.launchIn(unconfinedScope)
pushManager
.syncOrgKeysFlow
.onEach {
@@ -593,7 +538,8 @@ class AuthRepositoryImpl(
),
)
}
settingsRepository.storeUserHasLoggedInValue(userId)
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
vaultRepository.syncIfNecessary()
return LoginResult.Success
}
@@ -658,7 +604,6 @@ class AuthRepositoryImpl(
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
@@ -668,7 +613,6 @@ class AuthRepositoryImpl(
twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
@@ -813,7 +757,6 @@ class AuthRepositoryImpl(
)
.flatMap { registerKeyResponse ->
if (emailVerificationToken == null) {
// TODO PM-6675: Remove register call and service implementation
identityService.register(
body = RegisterRequestJson(
email = email,
@@ -872,6 +815,10 @@ class AuthRepositoryImpl(
?: it.message,
)
}
is RegisterResponseJson.Error -> {
RegisterResult.Error(it.message)
}
}
},
onFailure = { RegisterResult.Error(errorMessage = null) },
@@ -890,46 +837,6 @@ class AuthRepositoryImpl(
)
}
override suspend fun removePassword(masterPassword: String): RemovePasswordResult {
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return RemovePasswordResult.Error
val profile = activeAccount.profile
val userId = profile.userId
val userKey = authDiskSource
.getUserKey(userId = userId)
?: return RemovePasswordResult.Error
val keyConnectorUrl = organizations
.find {
it.shouldUseKeyConnector &&
it.type != OrganizationType.OWNER &&
it.type != OrganizationType.ADMIN
}
?.keyConnectorUrl
?: return RemovePasswordResult.Error
return keyConnectorManager
.migrateExistingUserToKeyConnector(
userId = userId,
url = keyConnectorUrl,
userKeyEncrypted = userKey,
email = profile.email,
masterPassword = masterPassword,
kdf = profile.toSdkParams(),
)
.onSuccess {
authDiskSource.userState = authDiskSource
.userState
?.toRemovedPasswordUserStateJson(userId = userId)
vaultRepository.sync()
settingsRepository.setDefaultsIfNecessary(userId = userId)
}
.fold(
onFailure = { RemovePasswordResult.Error },
onSuccess = { RemovePasswordResult.Success },
)
}
override suspend fun resetPassword(
currentPassword: String?,
newPassword: String,
@@ -1030,7 +937,7 @@ class AuthRepositoryImpl(
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
null,
-> {
-> {
authSdkSource
.makeRegisterKeys(
email = activeAccount.profile.email,
@@ -1080,7 +987,7 @@ class AuthRepositoryImpl(
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
-> {
IllegalStateException("Failed to unlock vault").asFailure()
}
}
@@ -1298,40 +1205,6 @@ class AuthRepositoryImpl(
},
)
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
return identityService
.verifyEmailRegistrationToken(
body = VerifyEmailTokenRequestJson(
email = email,
token = token,
),
)
.fold(
onSuccess = {
when (val json = it) {
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
is VerifyEmailTokenResponseJson.Invalid -> {
EmailTokenResult.Error(json.message)
}
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
}
},
onFailure = {
EmailTokenResult.Error(message = null)
},
)
}
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
override fun setShowImportLogins(showImportLogins: Boolean) {
val userId: String = activeUserId ?: return
authDiskSource.storeShowImportLogins(userId = userId, showImportLogins = showImportLogins)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,
@@ -1496,12 +1369,7 @@ class AuthRepositoryImpl(
captchaToken = captchaToken,
)
.fold(
onFailure = {
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
false -> LoginResult.UnofficialServerError
else -> LoginResult.Error(errorMessage = null)
}
},
onFailure = { LoginResult.Error(errorMessage = null) },
onSuccess = { loginResponse ->
when (loginResponse) {
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
@@ -1524,7 +1392,7 @@ class AuthRepositoryImpl(
)
is GetTokenResponseJson.Invalid -> LoginResult.Error(
errorMessage = loginResponse.errorMessage,
errorMessage = loginResponse.errorModel.errorMessage,
)
}
},
@@ -1545,42 +1413,30 @@ class AuthRepositoryImpl(
previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository.environment.environmentUrlData,
)
val profile = userStateJson.activeAccount.profile
val userId = profile.userId
val userId = userStateJson.activeUserId
checkForVaultUnlockError(
onVaultUnlockError = { vaultUnlockError ->
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
},
) {
val keyConnectorUrl = loginResponse
.keyConnectorUrl
?: loginResponse
.userDecryptionOptions
?.keyConnectorUserDecryptionOptions
?.keyConnectorUrl
val isDeviceUnlockAvailable = deviceData != null ||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
// if possible attempt to unlock the vault with trusted device data
if (isDeviceUnlockAvailable) {
unlockVaultWithTdeOnLoginSuccess(
loginResponse = loginResponse,
profile = profile,
userStateJson = userStateJson,
deviceData = deviceData,
)
} else if (keyConnectorUrl != null && orgIdentifier != null) {
unlockVaultWithKeyConnectorOnLoginSuccess(
profile = profile,
keyConnectorUrl = keyConnectorUrl,
orgIdentifier = orgIdentifier,
loginResponse = loginResponse,
)
} else {
unlockVaultWithPasswordOnLoginSuccess(
loginResponse = loginResponse,
profile = profile,
password = password,
)
password?.let {
unlockVaultWithPasswordOnLoginSuccess(
loginResponse = loginResponse,
userStateJson = userStateJson,
password = it,
)
}
}
}
@@ -1590,7 +1446,7 @@ class AuthRepositoryImpl(
.hashPassword(
email = email,
password = it,
kdf = profile.toSdkParams(),
kdf = userStateJson.activeAccount.profile.toSdkParams(),
purpose = HashPurpose.LOCAL_AUTHORIZATION,
)
.onSuccess { passwordHash ->
@@ -1612,27 +1468,14 @@ class AuthRepositoryImpl(
),
)
settingsRepository.hasUserLoggedInOrCreatedAccount = true
val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) &&
!settingsRepository.getUserHasLoggedInValue(userId = userId)
if (shouldSetOnboardingStatus) {
setOnboardingStatus(
userId = userId,
status = OnboardingStatus.NOT_STARTED,
)
}
authDiskSource.userState = userStateJson
loginResponse.key?.let {
// Only set the value if it's present, since we may have set it already
// when we completed the pending admin auth request.
authDiskSource.storeUserKey(userId = userId, userKey = it)
}
loginResponse.privateKey?.let {
// Only set the value if it's present, since we may have set it already
// when we completed the key connector conversion.
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
}
authDiskSource.storePrivateKey(userId = userId, privateKey = loginResponse.privateKey)
// If the user just authenticated with a two-factor code and selected the option to
// remember it, then the API response will return a token that will be used in place
// of the two-factor code on the next login attempt.
@@ -1651,7 +1494,6 @@ class AuthRepositoryImpl(
resendEmailRequestJson = null
twoFactorDeviceData = null
settingsRepository.setDefaultsIfNecessary(userId = userId)
settingsRepository.storeUserHasLoggedInValue(userId)
vaultRepository.syncIfNecessary()
hasPendingAccountAddition = false
LoginResult.Success
@@ -1682,89 +1524,12 @@ class AuthRepositoryImpl(
return LoginResult.TwoFactorRequired
}
/**
* Attempt to unlock the current user's vault with key connector data.
*/
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
profile: AccountJson.Profile,
keyConnectorUrl: String,
orgIdentifier: String,
loginResponse: GetTokenResponseJson.Success,
): VaultUnlockResult? =
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
// This user has a master password, so we skip the key-connector logic as it is not
// setup yet. The user can still unlock the vault with their master password.
null
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
// This is a returning user who should already have the key connector setup
keyConnectorManager
.getMasterKeyFromKeyConnector(
url = keyConnectorUrl,
accessToken = loginResponse.accessToken,
)
.map {
unlockVault(
accountProfile = profile,
privateKey = loginResponse.privateKey,
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
masterKey = it.masterKey,
userKey = loginResponse.key,
),
)
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { it },
)
} else {
// This is a new user who needs to setup the key connector
keyConnectorManager
.migrateNewUserToKeyConnector(
url = keyConnectorUrl,
accessToken = loginResponse.accessToken,
kdfType = loginResponse.kdfType,
kdfIterations = loginResponse.kdfIterations,
kdfMemory = loginResponse.kdfMemory,
kdfParallelism = loginResponse.kdfParallelism,
organizationIdentifier = orgIdentifier,
)
.map { keyConnectorResponse ->
val result = unlockVault(
accountProfile = profile,
privateKey = keyConnectorResponse.keys.private,
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
masterKey = keyConnectorResponse.masterKey,
userKey = keyConnectorResponse.encryptedUserKey,
),
)
if (result is VaultUnlockResult.Success) {
// We now know that login/unlock was successful, so we store the userKey
// and privateKey we now have since it didn't exist on the loginResponse
authDiskSource.storeUserKey(
userId = profile.userId,
userKey = keyConnectorResponse.encryptedUserKey,
)
authDiskSource.storePrivateKey(
userId = profile.userId,
privateKey = keyConnectorResponse.keys.private,
)
}
result
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { it },
)
}
/**
* Attempt to unlock the current user's vault with password data.
*/
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
loginResponse: GetTokenResponseJson.Success,
profile: AccountJson.Profile,
userStateJson: UserStateJson,
password: String?,
): VaultUnlockResult? {
// Attempt to unlock the vault with password if possible.
@@ -1772,7 +1537,7 @@ class AuthRepositoryImpl(
val privateKey = loginResponse.privateKey ?: return null
val key = loginResponse.key ?: return null
return unlockVault(
accountProfile = profile,
accountProfile = userStateJson.activeAccount.profile,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
@@ -1786,7 +1551,7 @@ class AuthRepositoryImpl(
*/
private suspend fun unlockVaultWithTdeOnLoginSuccess(
loginResponse: GetTokenResponseJson.Success,
profile: AccountJson.Profile,
userStateJson: UserStateJson,
deviceData: DeviceDataModel?,
): VaultUnlockResult? {
// Attempt to unlock the vault with auth request if possible.
@@ -1794,7 +1559,7 @@ class AuthRepositoryImpl(
if (loginResponse.privateKey != null && loginResponse.key != null) {
deviceData?.let { model ->
return unlockVault(
accountProfile = profile,
accountProfile = userStateJson.activeAccount.profile,
privateKey = loginResponse.privateKey,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = model.privateKey,
@@ -1823,7 +1588,7 @@ class AuthRepositoryImpl(
loginResponse.privateKey?.let { privateKey ->
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
options = options,
profile = profile,
userStateJson = userStateJson,
privateKey = privateKey,
)
}
@@ -1836,11 +1601,11 @@ class AuthRepositoryImpl(
*/
private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
options: TrustedDeviceUserDecryptionOptionsJson,
profile: AccountJson.Profile,
userStateJson: UserStateJson,
privateKey: String,
): VaultUnlockResult? {
var vaultUnlockResult: VaultUnlockResult? = null
val userId = profile.userId
val userId = userStateJson.activeUserId
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
if (deviceKey == null) {
// A null device key means this device is not trusted.
@@ -1854,7 +1619,7 @@ class AuthRepositoryImpl(
// For approved requests the key will always be present.
val userKey = requireNotNull(request.key)
vaultUnlockResult = unlockVault(
accountProfile = profile,
accountProfile = userStateJson.activeAccount.profile,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = pendingRequest.requestPrivateKey,
@@ -1881,7 +1646,7 @@ class AuthRepositoryImpl(
}
vaultUnlockResult = unlockVault(
accountProfile = profile,
accountProfile = userStateJson.activeAccount.profile,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
deviceKey = deviceKey,

View File

@@ -8,15 +8,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@@ -48,20 +44,16 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
configDiskSource: ConfigDiskSource,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
keyConnectorManager: KeyConnectorManager,
authRequestManager: AuthRequestManager,
trustedDeviceManager: TrustedDeviceManager,
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
policyManager: PolicyManager,
featureFlagManager: FeatureFlagManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService,
devicesService = devicesService,
@@ -70,20 +62,16 @@ object AuthRepositoryModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
configDiskSource = configDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatcherManager,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
keyConnectorManager = keyConnectorManager,
authRequestManager = authRequestManager,
trustedDeviceManager = trustedDeviceManager,
userLogoutManager = userLogoutManager,
pushManager = pushManager,
policyManager = policyManager,
featureFlagManager = featureFlagManager,
firstTimeActionManager = firstTimeActionManager,
logsManager = logsManager,
)
}

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Model the result of a request to validate a given email token.
*/
sealed class EmailTokenResult {
/**
* The token is valid and the user can proceed with account creation.
*/
data object Success : EmailTokenResult()
/**
* The token has expired and is no longer valid.
*/
data object Expired : EmailTokenResult()
/**
* There was an error validating the token.
*/
data class Error(val message: String?) : EmailTokenResult()
}

View File

@@ -37,10 +37,4 @@ data class JwtTokenDataJson(
@SerialName("amr")
val authenticationMethodsReference: List<String>,
) {
/**
* Indicates that this is an external user. Mainly used for SSO users with a key connector.
*/
val isExternal: Boolean
get() = authenticationMethodsReference.any { it == "external" }
}
)

View File

@@ -23,9 +23,4 @@ sealed class LoginResult {
* There was an error logging in.
*/
data class Error(val errorMessage: String?) : LoginResult()
/**
* There was an error while logging into an unofficial Bitwarden server.
*/
data object UnofficialServerError : LoginResult()
}

View File

@@ -11,5 +11,5 @@ fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)
-> LoginResult.Error(errorMessage = null)
}

View File

@@ -1,21 +1,12 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
/**
* Represents an organization a user may be a member of.
*
* @property id The ID of the organization.
* @property name The name of the organization (if applicable).
* @property shouldManageResetPassword Indicates that this user has the permission to manage their
* own password.
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
* @property role The user's role in the organization.
*/
data class Organization(
val id: String,
val name: String?,
val shouldManageResetPassword: Boolean,
val shouldUseKeyConnector: Boolean,
val role: OrganizationType,
)

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -46,7 +47,7 @@ sealed class PolicyInformation {
/**
* Represents a policy enforcing rules on the password generator.
*
* @property overridePasswordType The default type of password to be generated.
* @property defaultType The default type of password to be generated.
* @property minLength The minimum length of the password.
* @property useUpper Whether the password requires upper case letters.
* @property useLower Whether the password requires lower case letters.
@@ -60,8 +61,8 @@ sealed class PolicyInformation {
*/
@Serializable
data class PasswordGenerator(
@SerialName("overridePasswordType")
val overridePasswordType: String?,
@SerialName("defaultType")
val defaultType: String?,
@SerialName("minLength")
val minLength: Int?,
@@ -93,7 +94,6 @@ sealed class PolicyInformation {
@SerialName("includeNumber")
val includeNumber: Boolean?,
) : PolicyInformation() {
@Suppress("UndocumentedPublicClass")
companion object {
const val TYPE_PASSWORD: String = "password"
const val TYPE_PASSPHRASE: String = "passphrase"

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of removing a user's password.
*/
sealed class RemovePasswordResult {
/**
* The password was removed successfully.
*/
data object Success : RemovePasswordResult()
/**
* There was an error removing the password.
*/
data object Error : RemovePasswordResult()
}

View File

@@ -1,9 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Associates [isUsingKeyConnector] with the given [userId].
*/
data class UserKeyConnectorState(
val userId: String,
val isUsingKeyConnector: Boolean?,
)

View File

@@ -1,8 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
/**
@@ -30,9 +28,6 @@ data class UserState(
val activeAccount: Account
get() = accounts.first { it.userId == activeUserId }
val activeUserFirstTimeState: FirstTimeState
get() = activeAccount.firstTimeState
/**
* Basic account information about a given user.
*
@@ -50,12 +45,10 @@ data class UserState(
* they logged in using SSO and don't yet have one). NOTE: This should **not** be used to
* determine whether a user has a master password. There are cases in which a user can both
* not have a password but still not need one, such as TDE.
* @property hasMasterPassword Indicates that the user does or does not have a master password.
* @property organizations List of [Organization]s the user is associated with, if any.
* @property isBiometricsEnabled Indicates that the biometrics mechanism for unlocking the
* user's vault is enabled.
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
* @property isUsingKeyConnector Indicates if the account is currently using a key connector.
*/
data class Account(
val userId: String,
@@ -68,15 +61,16 @@ data class UserState(
val isVaultUnlocked: Boolean,
val needsPasswordReset: Boolean,
val needsMasterPassword: Boolean,
val hasMasterPassword: Boolean,
val trustedDevice: TrustedDevice?,
val organizations: List<Organization>,
val isBiometricsEnabled: Boolean,
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
val isUsingKeyConnector: Boolean,
val onboardingStatus: OnboardingStatus,
val firstTimeState: FirstTimeState,
) {
/**
* Indicates that the user does or does not have a master password.
*/
val hasMasterPassword: Boolean get() = trustedDevice?.hasMasterPassword != false
/**
* Indicates that the user does or does not have a means to manually unlock the vault.
*/
@@ -92,6 +86,7 @@ data class UserState(
*/
data class TrustedDevice(
val isDeviceTrusted: Boolean,
val hasMasterPassword: Boolean,
val hasAdminApproval: Boolean,
val hasLoginApprovingDevice: Boolean,
val hasResetPasswordPermission: Boolean,

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserSwitchingData
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -11,7 +9,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
/**
@@ -103,47 +100,6 @@ val AuthDiskSource.userAccountTokensFlow: Flow<List<UserAccountTokens>>
}
.distinctUntilChanged()
/**
* Returns the current list of [UserKeyConnectorState].
*/
val AuthDiskSource.userKeyConnectorStateList: List<UserKeyConnectorState>
get() = this
.userState
?.accounts
.orEmpty()
.map { (userId, _) ->
UserKeyConnectorState(
userId = userId,
isUsingKeyConnector = this.getShouldUseKeyConnector(userId = userId),
)
}
/**
* Returns a [Flow] that emits distinct updates to [UserKeyConnectorState].
*/
@OptIn(ExperimentalCoroutinesApi::class)
val AuthDiskSource.userKeyConnectorStateFlow: Flow<List<UserKeyConnectorState>>
get() = this
.userStateFlow
.flatMapLatest { userStateJson ->
combine(
userStateJson
?.accounts
.orEmpty()
.map { (userId, _) ->
this
.getShouldUseKeyConnectorFlow(userId = userId)
.map {
UserKeyConnectorState(
userId = userId,
isUsingKeyConnector = it,
)
}
},
) { it.toList() }
}
.distinctUntilChanged()
/**
* Returns a [Flow] that emits every time the active user is changed.
*/
@@ -169,25 +125,3 @@ val AuthDiskSource.activeUserIdChangesFlow: Flow<String?>
.userStateFlow
.map { it?.activeUserId }
.distinctUntilChanged()
/**
* Returns a [Flow] that emits every time the active user's onboarding status is changed
*/
@OptIn(ExperimentalCoroutinesApi::class)
val AuthDiskSource.onboardingStatusChangesFlow: Flow<OnboardingStatus?>
get() = activeUserIdChangesFlow
.flatMapLatest { activeUserId ->
activeUserId
?.let { this.getOnboardingStatusFlow(userId = it) }
?: flowOf(null)
}
.distinctUntilChanged()
/**
* Returns the current [OnboardingStatus] of the active user.
*/
val AuthDiskSource.currentOnboardingStatus: OnboardingStatus?
get() = this
.userState
?.activeUserId
?.let { this.getOnboardingStatus(userId = it) }

View File

@@ -7,11 +7,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.serialization.json.Json
private val JSON = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
/**
* Maps the given [SyncResponseJson.Profile.Organization] to an [Organization].
*/
@@ -19,9 +14,6 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
Organization(
id = this.id,
name = this.name,
shouldUseKeyConnector = this.shouldUseKeyConnector,
role = this.type,
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
)
/**
@@ -36,22 +28,21 @@ fun List<SyncResponseJson.Profile.Organization>.toOrganizations(): List<Organiza
*/
val SyncResponseJson.Policy.policyInformation: PolicyInformation?
get() = data?.toString()?.let {
when (type) {
PolicyTypeJson.MASTER_PASSWORD -> {
JSON.decodeFromStringOrNull<PolicyInformation.MasterPassword>(it)
Json.decodeFromStringOrNull<PolicyInformation.MasterPassword>(it)
}
PolicyTypeJson.PASSWORD_GENERATOR -> {
JSON.decodeFromStringOrNull<PolicyInformation.PasswordGenerator>(it)
Json.decodeFromStringOrNull<PolicyInformation.PasswordGenerator>(it)
}
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
JSON.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
Json.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
}
PolicyTypeJson.SEND_OPTIONS -> {
JSON.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
Json.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
}
else -> null

View File

@@ -1,47 +1,17 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import com.x8bit.bitwarden.ui.platform.base.util.toHexColorRepresentation
/**
* Updates the given [UserStateJson] with the data to indicate that the password has been removed.
* The original will be returned if the [userId] does not match any accounts in the [UserStateJson].
*/
fun UserStateJson.toRemovedPasswordUserStateJson(
userId: String,
): UserStateJson {
val account = this.accounts[userId] ?: return this
val profile = account.profile
val updatedUserDecryptionOptions = profile
.userDecryptionOptions
?.copy(hasMasterPassword = false)
?: UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = null,
keyConnectorUserDecryptionOptions = null,
)
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
val updatedAccount = account.copy(profile = updatedProfile)
return this.copy(
accounts = accounts
.toMutableMap()
.apply { replace(userId, updatedAccount) },
)
}
/**
* Updates the given [UserStateJson] with the data from the [syncResponse] to return a new
* [UserStateJson]. The original will be returned if the sync response does not match any accounts
@@ -104,15 +74,12 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
/**
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
*/
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongParameterList")
fun UserStateJson.toUserState(
vaultState: List<VaultUnlockData>,
userAccountTokens: List<UserAccountTokens>,
userOrganizationsList: List<UserOrganizations>,
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
hasPendingAccountAddition: Boolean,
onboardingStatus: OnboardingStatus?,
firstTimeState: FirstTimeState,
isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
@@ -130,31 +97,19 @@ fun UserStateJson.toUserState(
val decryptionOptions = profile.userDecryptionOptions
val trustedDeviceOptions = decryptionOptions?.trustedDeviceUserDecryptionOptions
val keyConnectorOptions = decryptionOptions?.keyConnectorUserDecryptionOptions
val organizations = userOrganizationsList
.find { it.userId == userId }
?.organizations
.orEmpty()
val hasManageResetPasswordPermission = organizations.any {
it.role == OrganizationType.OWNER ||
it.role == OrganizationType.ADMIN ||
it.shouldManageResetPassword
}
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
trustedDeviceOptions?.hasManageResetPasswordPermission != false &&
keyConnectorOptions == null
val trustedDevice = trustedDeviceOptions?.let {
UserState.TrustedDevice(
isDeviceTrusted = isDeviceTrustedProvider(userId),
hasMasterPassword = decryptionOptions.hasMasterPassword,
hasAdminApproval = it.hasAdminApproval,
hasLoginApprovingDevice = it.hasLoginApprovingDevice,
hasResetPasswordPermission = it.hasManageResetPasswordPermission,
)
}
// If a user does not have a Master Password we want to check if they have another
// method for unlocking the vault. In the case of a TDE user we check if they
// have the reset password permission via their organization(S). If the user does
// not belong to a TDE or we check to see if they user key connector.
val tdeUserNeedsMasterPassword =
hasManageResetPasswordPermission.takeIf { trustedDevice != null }
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
(tdeUserNeedsMasterPassword ?: (keyConnectorOptions == null))
UserState.Account(
userId = userId,
name = profile.name,
@@ -170,19 +125,14 @@ fun UserStateJson.toUserState(
?.isLoggedIn == true,
isVaultUnlocked = vaultUnlocked,
needsPasswordReset = needsPasswordReset,
organizations = organizations,
organizations = userOrganizationsList
.find { it.userId == userId }
?.organizations
.orEmpty(),
isBiometricsEnabled = isBiometricsEnabledProvider(userId),
vaultUnlockType = vaultUnlockTypeProvider(userId),
needsMasterPassword = needsMasterPassword,
hasMasterPassword = decryptionOptions?.hasMasterPassword != false,
trustedDevice = trustedDevice,
isUsingKeyConnector = userIsUsingKeyConnectorList
.find { it.userId == userId }
?.isUsingKeyConnector == true,
// If the user exists with no onboarding status we can assume they have been
// using the app prior to the release of the onboarding flow.
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
firstTimeState = firstTimeState,
)
},
hasPendingAccountAddition = hasPendingAccountAddition,

View File

@@ -9,11 +9,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
* The [CompleteRegistrationData] will be returned when present.
*/
fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? {
val sanitizedUriString = data.toString().replace(
oldValue = "/redirect-connector.html#",
newValue = "/",
ignoreCase = true,
)
val sanitizedUriString = data.toString().replace("/#/", "/")
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
uri.host ?: return null
if (uri.path != "/finish-signup") return null

View File

@@ -1,30 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* The [AccessibilityService] implementation for the app. This is not used in the traditional
* way, we use the [BitwardenAutofillTileService] to invoke this service in order to provide an
* autofill fallback mechanism.
*/
@Keep
@OmitFromCoverage
@AndroidEntryPoint
class BitwardenAccessibilityService : AccessibilityService() {
@Inject
lateinit var processor: BitwardenAccessibilityProcessor
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (rootInActiveWindow?.packageName != event.packageName) return
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source)
}
override fun onInterrupt() = Unit
}

View File

@@ -1,118 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context
import android.content.pm.PackageManager
import android.os.PowerManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParserImpl
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessorImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
* Provides dependencies within the accessibility package.
*/
@Module
@InstallIn(SingletonComponent::class)
object AccessibilityModule {
@Singleton
@Provides
fun providesAccessibilityCompletionManager(
accessibilityAutofillManager: AccessibilityAutofillManager,
totpManager: AutofillTotpManager,
dispatcherManager: DispatcherManager,
): AccessibilityCompletionManager =
AccessibilityCompletionManagerImpl(
accessibilityAutofillManager = accessibilityAutofillManager,
totpManager = totpManager,
dispatcherManager = dispatcherManager,
)
@Singleton
@Provides
fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
AccessibilityAutofillManagerImpl()
@Singleton
@Provides
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl()
@Singleton
@Provides
fun providesAccessibilityNodeInfoManager(): AccessibilityNodeInfoManager =
AccessibilityNodeInfoManagerImpl()
@Singleton
@Provides
fun providesAccessibilityParser(
accessibilityNodeInfoManager: AccessibilityNodeInfoManager,
): AccessibilityParser = AccessibilityParserImpl(
accessibilityNodeInfoManager = accessibilityNodeInfoManager,
)
@Singleton
@Provides
fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager =
AccessibilitySelectionManagerImpl()
@Singleton
@Provides
fun providesBitwardenAccessibilityProcessor(
@ApplicationContext context: Context,
accessibilityParser: AccessibilityParser,
accessibilityAutofillManager: AccessibilityAutofillManager,
launcherPackageNameManager: LauncherPackageNameManager,
powerManager: PowerManager,
): BitwardenAccessibilityProcessor =
BitwardenAccessibilityProcessorImpl(
context = context,
accessibilityParser = accessibilityParser,
accessibilityAutofillManager = accessibilityAutofillManager,
launcherPackageNameManager = launcherPackageNameManager,
powerManager = powerManager,
)
@Singleton
@Provides
fun providesLauncherPackageNameManager(
clock: Clock,
packageManager: PackageManager,
): LauncherPackageNameManager =
LauncherPackageNameManagerImpl(
clockProvider = { clock },
packageManager = packageManager,
)
@Singleton
@Provides
fun providesPackageManager(
@ApplicationContext context: Context,
): PackageManager = context.packageManager
@Singleton
@Provides
fun providesPowerManager(
@ApplicationContext context: Context,
): PowerManager = context.getSystemService(PowerManager::class.java)
}

View File

@@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
/**
* Provides dependencies within the accessibility package scoped to the activity.
*/
@Module
@InstallIn(ActivityComponent::class)
object ActivityAccessibilityModule {
@ActivityScoped
@Provides
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager,
lifecycleScope = lifecycleScope,
)
}

View File

@@ -1,10 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.app.Activity
/**
* A helper for dealing with accessibility configuration that must be scoped to a specific
* [Activity]. In particular, this should be injected into an [Activity] to ensure that the
* [AccessibilityEnabledManager] reports correct values.
*/
interface AccessibilityActivityManager

View File

@@ -1,28 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* The default implementation of the [AccessibilityActivityManager].
*/
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appForegroundManager
.appForegroundStateFlow
.onEach {
accessibilityEnabledManager.isAccessibilityEnabled =
context.isAccessibilityServiceEnabled
}
.launchIn(lifecycleScope)
}
}

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
/**
* A relay manager used to notify the accessibility service to attempt an autofill.
*/
interface AccessibilityAutofillManager {
/**
* Indicates that the Autofill tile has been clicked and we attempt an accessibility-based
* autofill.
*/
var accessibilityAction: AccessibilityAction?
}

View File

@@ -1,10 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
/**
* The default implementation for the [AccessibilityAutofillManager].
*/
class AccessibilityAutofillManagerImpl : AccessibilityAutofillManager {
override var accessibilityAction: AccessibilityAction? = null
}

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.app.Activity
import com.bitwarden.vault.CipherView
/**
* A manager for completing the accessibility-based autofill process after the user has made a
* selection.
*/
interface AccessibilityCompletionManager {
/**
* Completes the accessibility-based autofill flow originating with the given [activity] using
* the selected [cipherView].
*/
fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView)
}

View File

@@ -1,53 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.app.Activity
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Default implementation for the [AccessibilityCompletionManager].
*/
class AccessibilityCompletionManagerImpl(
private val accessibilityAutofillManager: AccessibilityAutofillManager,
private val totpManager: AutofillTotpManager,
dispatcherManager: DispatcherManager,
) : AccessibilityCompletionManager {
private val mainScope = CoroutineScope(dispatcherManager.main)
override fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView) {
val autofillSelectionData = activity
.intent
?.getAutofillSelectionDataOrNull()
?: run {
activity.finishAndRemoveTask()
return
}
if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) {
activity.finishAndRemoveTask()
return
}
val uri = autofillSelectionData
.uri
?.toUriOrNull()
?: run {
activity.finishAndRemoveTask()
return
}
accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptFill(
cipherView = cipherView,
uri = uri,
)
mainScope.launch {
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
}
activity.finishAndRemoveTask()
}
}

View File

@@ -1,21 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import kotlinx.coroutines.flow.StateFlow
/**
* A container for values specifying whether or not the accessibility service is enabled.
*/
interface AccessibilityEnabledManager {
/**
* Whether or not the accessibility service should be considered enabled.
*
* Note that changing this does not enable or disable autofill; it is only an indicator that
* this has occurred elsewhere.
*/
var isAccessibilityEnabled: Boolean
/**
* Emits updates that track [isAccessibilityEnabled] values.
*/
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
}

View File

@@ -1,21 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* The default implementation of [AccessibilityEnabledManager].
*/
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
override var isAccessibilityEnabled: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) {
mutableIsAccessibilityEnabledStateFlow.value = value
}
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
}

View File

@@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
/**
* The default maximum recursive depth that the
* [AccessibilityNodeInfoManager.findAccessibilityNodeInfoList] will go.
*/
const val DEFAULT_MAX_RECURSION_DEPTH: Int = 100
/**
* A manager for finding fields that match particular characteristics.
*/
interface AccessibilityNodeInfoManager {
/**
* A helper function for retrieving the appropriate nodes based on the given [predicate].
*
* This function is recursive but will stop recurring if the depth it reaches is greater than
* the [maxRecursionDepth].
*/
fun findAccessibilityNodeInfoList(
rootNode: AccessibilityNodeInfo,
maxRecursionDepth: Int = DEFAULT_MAX_RECURSION_DEPTH,
predicate: (AccessibilityNodeInfo) -> Boolean,
): List<AccessibilityNodeInfo>
/**
* Determines which [AccessibilityNodeInfo] is a username field.
*/
fun findUsernameAccessibilityNodeInfo(
uri: Uri,
allNodes: List<AccessibilityNodeInfo>,
passwordNodes: List<AccessibilityNodeInfo>,
): AccessibilityNodeInfo?
}

View File

@@ -1,94 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
import com.x8bit.bitwarden.data.autofill.accessibility.util.getKnownUsernameFieldNull
import com.x8bit.bitwarden.data.autofill.accessibility.util.isUsername
import timber.log.Timber
private const val MAX_NODE_COUNT: Int = 100
/**
* The default implementation for the [AccessibilityNodeInfoManager].
*/
class AccessibilityNodeInfoManagerImpl : AccessibilityNodeInfoManager {
override fun findAccessibilityNodeInfoList(
rootNode: AccessibilityNodeInfo,
maxRecursionDepth: Int,
predicate: (AccessibilityNodeInfo) -> Boolean,
): List<AccessibilityNodeInfo> =
findAccessibilityNodeInfoList(
rootNode = rootNode,
maxRecursionDepth = maxRecursionDepth,
currentRecursionDepth = 0,
predicate = predicate,
)
override fun findUsernameAccessibilityNodeInfo(
uri: Uri,
allNodes: List<AccessibilityNodeInfo>,
passwordNodes: List<AccessibilityNodeInfo>,
): AccessibilityNodeInfo? {
val uriPath = uri
.path
?: return findMissingUsernameNodeInfo(
allNodes = allNodes,
passwordNodes = passwordNodes,
)
return uri
.authority
?.removePrefix(prefix = "www.")
?.getKnownUsernameFieldNull()
?.let { usernameField ->
allNodes.firstOrNull { node ->
node.isUsername(
uriPath = uriPath,
knownUsernameField = usernameField,
)
}
}
?: findMissingUsernameNodeInfo(allNodes = allNodes, passwordNodes = passwordNodes)
}
private fun findAccessibilityNodeInfoList(
rootNode: AccessibilityNodeInfo,
maxRecursionDepth: Int,
currentRecursionDepth: Int,
predicate: (AccessibilityNodeInfo) -> Boolean,
): List<AccessibilityNodeInfo> {
if (predicate(rootNode)) return listOf(rootNode)
if (currentRecursionDepth >= maxRecursionDepth) return emptyList()
val childNodeCount = rootNode.childCount - 1
if (childNodeCount > MAX_NODE_COUNT) log(message = "Too many child iterations.")
return (0..childNodeCount.coerceAtMost(maximumValue = MAX_NODE_COUNT)).flatMap {
val childNode = rootNode.getChild(it) ?: return@flatMap emptyList()
if (childNode.hashCode() == this.hashCode()) {
log(message = "Child node is the same as parent for some reason.")
emptyList()
} else {
findAccessibilityNodeInfoList(
rootNode = childNode,
maxRecursionDepth = maxRecursionDepth,
currentRecursionDepth = currentRecursionDepth + 1,
predicate = predicate,
)
}
}
}
/**
* Attempts to find a username [AccessibilityNodeInfo] if there isn't one already. This
* functions by finding the first known password node and taking the node directly above it.
*/
private fun findMissingUsernameNodeInfo(
allNodes: List<AccessibilityNodeInfo>,
passwordNodes: List<AccessibilityNodeInfo>,
): AccessibilityNodeInfo? =
passwordNodes
.firstOrNull()
?.let { allNodes.getOrNull(index = allNodes.indexOf(element = it) - 1) }
private fun log(message: String) {
Timber.i(message)
}
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import com.bitwarden.vault.CipherView
import kotlinx.coroutines.flow.Flow
/**
* A manager class used to handle the accessibility autofill selections.
*/
interface AccessibilitySelectionManager {
/**
* Emits a [CipherView] as a result of calls to [emitAccessibilitySelection].
*/
val accessibilitySelectionFlow: Flow<CipherView>
/**
* Triggers an emission via [accessibilitySelectionFlow].
*/
fun emitAccessibilitySelection(cipherView: CipherView)
}

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import com.bitwarden.vault.CipherView
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
/**
* The default implementation of the [AccessibilitySelectionManager].
*/
class AccessibilitySelectionManagerImpl : AccessibilitySelectionManager {
private val accessibilitySelectionChannel: Channel<CipherView> = Channel(
capacity = Int.MAX_VALUE,
)
override val accessibilitySelectionFlow: Flow<CipherView> =
accessibilitySelectionChannel.receiveAsFlow()
override fun emitAccessibilitySelection(cipherView: CipherView) {
accessibilitySelectionChannel.trySend(cipherView)
}
}

View File

@@ -1,11 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
/**
* A manager for getting the launcher packages from the operating system.
*/
interface LauncherPackageNameManager {
/**
* A list of launcher packages from the operating system.
*/
val launcherPackages: List<String>
}

View File

@@ -1,41 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Intent
import android.content.pm.PackageManager
import java.time.Clock
/**
* How frequently the cached launcher list should be refreshed.
*/
private const val REFRESH_CACHE_MS: Long = 1L * 60L * 60L * 1000L
/**
* The default implementation of the [LauncherPackageNameManager].
*/
class LauncherPackageNameManagerImpl(
private val clockProvider: () -> Clock,
private val packageManager: PackageManager,
) : LauncherPackageNameManager {
private var lastLauncherFetchMs: Long = 0L
private var cachedLauncherPackages: List<String>? = null
override val launcherPackages: List<String>
get() {
if (cachedLauncherPackages == null ||
clockProvider().millis() - lastLauncherFetchMs > REFRESH_CACHE_MS
) {
updateCachedLauncherPackages()
}
return cachedLauncherPackages.orEmpty()
}
private fun updateCachedLauncherPackages() {
cachedLauncherPackages = packageManager
.queryIntentActivities(
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
0,
)
.map { it.activityInfo.packageName }
lastLauncherFetchMs = clockProvider().millis()
}
}

View File

@@ -1,24 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.model
import android.net.Uri
import com.bitwarden.vault.CipherView
/**
*Represents an action to be taken by the accessibility service.
*/
sealed class AccessibilityAction {
/**
* Indicates that the accessibility service should attempt to scan the currently foregrounded
* application for a [Uri].
*/
data object AttemptParseUri : AccessibilityAction()
/**
* Indicates that the accessibility service should attempt to scan the currently foregrounded
* application for a fields to fill.
*/
data class AttemptFill(
val cipherView: CipherView,
val uri: Uri,
) : AccessibilityAction()
}

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.model
/**
* A model representing a supported browser.
*/
data class Browser(
val packageName: String,
val possibleUrlFieldIds: List<String>,
val urlExtractor: (String) -> String? = { it },
) {
constructor(
packageName: String,
urlFieldId: String,
urlExtractor: (String) -> String? = { it },
) : this(
packageName = packageName,
possibleUrlFieldIds = listOf(urlFieldId),
urlExtractor = urlExtractor,
)
}

View File

@@ -1,11 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.model
import android.view.accessibility.AccessibilityNodeInfo
/**
* Represents the fillable fields for accessibility based autofill.
*/
data class FillableFields(
val usernameField: AccessibilityNodeInfo?,
val passwordFields: List<AccessibilityNodeInfo>,
)

View File

@@ -1,71 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.model
/**
* Represents the known username fields for a given [uriAuthority].
*/
data class KnownUsernameField(
val uriAuthority: String,
val accessOptions: List<AccessOptions>,
) {
constructor(
uriAuthority: String,
accessOption: AccessOptions,
) : this(uriAuthority = uriAuthority, accessOptions = listOf(accessOption))
}
/**
* Represents the view IDs for a given uri path.
*/
data class AccessOptions(
val matchValue: String,
val matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
val usernameViewIds: List<String>,
) {
constructor(
matchValue: String,
matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
usernameViewId: String,
) : this(
matchValue = matchValue,
matchingStrategy = matchingStrategy,
usernameViewIds = listOf(usernameViewId),
)
/**
* Indicates the matching strategy needed for the particular [AccessOptions].
*/
enum class MatchingStrategy(
val matches: (uriPath: String, matchValue: String) -> Boolean,
) {
CONTAINS_CASE_INSENSITIVE(
matches = { uriPath, matchValue ->
uriPath.contains(other = matchValue, ignoreCase = true)
},
),
CONTAINS_CASE_SENSITIVE(
matches = { uriPath, matchValue ->
uriPath.contains(other = matchValue, ignoreCase = false)
},
),
ENDS_WITH_CASE_INSENSITIVE(
matches = { uriPath, matchValue ->
uriPath.endsWith(suffix = matchValue, ignoreCase = true)
},
),
ENDS_WITH_CASE_SENSITIVE(
matches = { uriPath, matchValue ->
uriPath.endsWith(suffix = matchValue, ignoreCase = false)
},
),
STARTS_WITH_CASE_INSENSITIVE(
matches = { uriPath, matchValue ->
uriPath.startsWith(prefix = matchValue, ignoreCase = true)
},
),
STARTS_WITH_CASE_SENSITIVE(
matches = { uriPath, matchValue ->
uriPath.startsWith(prefix = matchValue, ignoreCase = false)
},
),
}
}

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.parser
import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
/**
* A tool for parsing accessibility data from the OS into domain models.
*/
interface AccessibilityParser {
/**
* Parses the fillable fields from [rootNode].
*/
fun parseForFillableFields(rootNode: AccessibilityNodeInfo, uri: Uri): FillableFields
/**
* Parses the [Uri] from [rootNode] and returns a url, package name.
*/
fun parseForUriOrPackageName(rootNode: AccessibilityNodeInfo): Uri?
}

Some files were not shown because too many files have changed in this diff Show More