mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 22:31:17 -05:00
Compare commits
6 Commits
v2025.2.0
...
pm-11304/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947eec9348 | ||
|
|
b8b9cf2950 | ||
|
|
3954f53af8 | ||
|
|
4a22627dba | ||
|
|
1a188cc73a | ||
|
|
d0d212791d |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -5,10 +5,10 @@
|
||||
# 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 @phil-livefront
|
||||
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront
|
||||
|
||||
# Actions and workflow changes.
|
||||
.github/ @bitwarden/dept-development-mobile
|
||||
.github/workflows @bitwarden/dept-development-mobile
|
||||
|
||||
# Auth
|
||||
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -15,5 +15,3 @@ contact_links:
|
||||
- name: Security Issues
|
||||
url: https://hackerone.com/bitwarden
|
||||
about: We use HackerOne to manage security disclosures.
|
||||
- name: Report mobile autofill failure
|
||||
url: https://docs.google.com/forms/d/e/1FAIpQLScMopHyN7KGJs8hW562VTzbIGL4KcFnx0wJcsW0GYE1BnPiGA/viewform
|
||||
|
||||
13
.github/workflows/build-authenticator.yml
vendored
13
.github/workflows/build-authenticator.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Build Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
placeholder:
|
||||
name: Placeholder Job
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Placeholder Step
|
||||
run: echo "placeholder workflow"
|
||||
119
.github/workflows/build.yml
vendored
119
.github/workflows/build.yml
vendored
@@ -40,10 +40,10 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-reports
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -157,10 +157,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@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -179,20 +179,11 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
|
||||
@@ -253,78 +244,78 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/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
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk" \
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
|
||||
> ./com.x8bit.bitwarden.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.beta.apk" \
|
||||
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
|
||||
> ./com.x8bit.bitwarden.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.aab" \
|
||||
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
|
||||
> ./com.x8bit.bitwarden.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.beta.aab" \
|
||||
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
|
||||
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for Debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk" \
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
|
||||
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -332,7 +323,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -340,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -348,7 +339,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -356,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -405,7 +396,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -442,10 +433,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@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -455,7 +446,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -464,35 +455,19 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
# Start from 11000 to prevent collisions with mobile build version codes
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
@@ -515,49 +490,49 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for F-Droid artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk" \
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload F-Droid Beta .apk artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.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.beta-fdroid.apk" \
|
||||
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
|
||||
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: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
|
||||
env:
|
||||
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
|
||||
run: |
|
||||
|
||||
13
.github/workflows/crowdin-pull-authenticator.yml
vendored
13
.github/workflows/crowdin-pull-authenticator.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Crowdin Sync - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
placeholder:
|
||||
name: Placeholder Job
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Placeholder Step
|
||||
run: echo "placeholder workflow"
|
||||
13
.github/workflows/crowdin-pull.yml
vendored
13
.github/workflows/crowdin-pull.yml
vendored
@@ -2,7 +2,7 @@ name: Crowdin Sync
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
inputs: { }
|
||||
schedule:
|
||||
- cron: '0 0 * * 5'
|
||||
|
||||
@@ -28,17 +28,10 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
|
||||
13
.github/workflows/crowdin-push-authenticator.yml
vendored
13
.github/workflows/crowdin-push-authenticator.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Crowdin Push - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
placeholder:
|
||||
name: Placeholder Job
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Placeholder Step
|
||||
run: echo "placeholder workflow"
|
||||
4
.github/workflows/crowdin-push.yml
vendored
4
.github/workflows/crowdin-push.yml
vendored
@@ -23,13 +23,13 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@2bd1450c2cdb2a8ac886232b8589696f22794229 # v0.2.0
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
129
.github/workflows/github-release.yml
vendored
129
.github/workflows/github-release.yml
vendored
@@ -1,129 +0,0 @@
|
||||
name: Create GitHub Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
description: 'Version Name - E.g. "2024.11.1"'
|
||||
required: true
|
||||
type: string
|
||||
version-number:
|
||||
description: 'Version Number - E.g. "123456"'
|
||||
required: true
|
||||
type: string
|
||||
artifact-run-id:
|
||||
description: 'GitHub Action Run ID containing artifacts'
|
||||
required: true
|
||||
type: string
|
||||
draft:
|
||||
description: 'Create as draft release'
|
||||
type: boolean
|
||||
default: true
|
||||
prerelease:
|
||||
description: 'Mark as pre-release'
|
||||
type: boolean
|
||||
default: true
|
||||
make-latest:
|
||||
description: 'Set as the latest release'
|
||||
type: boolean
|
||||
branch-protection-type:
|
||||
description: 'Branch protection type'
|
||||
type: choice
|
||||
options:
|
||||
- Branch Name
|
||||
- GitHub API
|
||||
default: Branch Name
|
||||
env:
|
||||
ARTIFACTS_PATH: artifacts
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get branch from workflow run
|
||||
id: get_release_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
|
||||
run: |
|
||||
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
|
||||
|
||||
case "$BRANCH_PROTECTION_TYPE" in
|
||||
"Branch Name")
|
||||
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
|
||||
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"GitHub API")
|
||||
#NOTE requires token with "administration:read" scope
|
||||
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
|
||||
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
run: |
|
||||
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
|
||||
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
|
||||
echo "Downloaded $file_count file(s)."
|
||||
if [ "$file_count" -gt 0 ]; then
|
||||
echo "Downloaded files:"
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
with:
|
||||
tag_name: "v${{ inputs.version-name }}"
|
||||
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
draft: ${{ inputs.draft }}
|
||||
make_latest: ${{ inputs.make-latest }}
|
||||
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*
|
||||
|
||||
- name: Update Release Description
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.create_release.outputs.id }}
|
||||
RELEASE_URL: ${{ steps.create_release.outputs.url }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
run: |
|
||||
# Get current release body
|
||||
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
|
||||
|
||||
# Append build source to the end
|
||||
updated_body="${current_body}
|
||||
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
|
||||
|
||||
# Update release
|
||||
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
|
||||
-f body="$updated_body"
|
||||
|
||||
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY
|
||||
58
.github/workflows/release-branch.yml
vendored
58
.github/workflows/release-branch.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Cut Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: 'Release Type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- RC
|
||||
- Hotfix
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
name: Create Release Branch
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
env:
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
run: |
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
branch_name="release/${current_date}-rc${{ github.run_number }}"
|
||||
else
|
||||
branch_name="release/rc${{ github.run_number }}"
|
||||
fi
|
||||
git switch main
|
||||
git switch -c $branch_name
|
||||
git push origin $branch_name
|
||||
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Create Hotfix Branch
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
run: |
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "::error::No tags found in the repository"
|
||||
exit 1
|
||||
fi
|
||||
branch_name="release/hotfix-${latest_tag}"
|
||||
echo "🌿 branch name: $branch_name"
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
|
||||
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
git switch -c $branch_name $latest_tag
|
||||
git push origin $branch_name
|
||||
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
13
.github/workflows/scan-authenticator.yml
vendored
13
.github/workflows/scan-authenticator.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Scan Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
placeholder:
|
||||
name: Placeholder Job
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Placeholder Step
|
||||
run: echo "placeholder workflow"
|
||||
61
.github/workflows/scan-ci.yml
vendored
61
.github/workflows/scan-ci.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Scan Protected Branches On Push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
17
.github/workflows/scan.yml
vendored
17
.github/workflows/scan.yml
vendored
@@ -1,9 +1,16 @@
|
||||
name: Scan Pull Requests
|
||||
name: Scan
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
@@ -26,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
uses: checkmarx/ast-github-action@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@@ -41,7 +48,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -61,11 +68,11 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
|
||||
uses: sonarsource/sonarcloud-github-action@383f7e52eae3ab0510c3cb0e7d9d150bbaeab838 # v3.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
|
||||
13
.github/workflows/test-authenticator.yml
vendored
13
.github/workflows/test-authenticator.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Test Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
placeholder:
|
||||
name: Placeholder Job
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Placeholder Step
|
||||
run: echo "placeholder workflow"
|
||||
82
.github/workflows/test.yml
vendored
82
.github/workflows/test.yml
vendored
@@ -6,33 +6,42 @@ on:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
type: [checks_requested]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
_JAVA_VERSION: 17
|
||||
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
packages: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -42,7 +51,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -51,15 +60,15 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env._JAVA_VERSION }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
@@ -68,58 +77,19 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Build and test
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
|
||||
run: |
|
||||
bundle exec fastlane check
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports
|
||||
path: |
|
||||
app/build/reports/tests/
|
||||
app/build/reports/kover/reportStandardDebug.xml
|
||||
|
||||
report:
|
||||
name: Process Test Reports
|
||||
needs: test
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
- 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
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
os: linux
|
||||
files: kover/reportStandardDebug.xml
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Comment PR if tests failed
|
||||
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
|
||||
file: app/build/reports/kover/reportStandardDebug.xml
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUN_ACTOR: ${{ github.triggering_actor }}
|
||||
run: |
|
||||
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
|
||||
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ ! -z "$PR_NUMBER" ]; then
|
||||
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Upload to codecov.io" step of [Process Test Reports job]('$_GITHUB_ACTION_RUN_URL') for more details.'
|
||||
gh pr comment --repo $GITHUB_REPOSITORY $PR_NUMBER --body "$message"
|
||||
fi
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
46
Gemfile.lock
46
Gemfile.lock
@@ -10,20 +10,20 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1040.0)
|
||||
aws-sdk-core (3.216.0)
|
||||
aws-partitions (1.1003.0)
|
||||
aws-sdk-core (3.212.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-s3 (1.170.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@@ -32,7 +32,7 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
date (3.4.1)
|
||||
date (3.4.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
@@ -59,8 +59,8 @@ GEM
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
@@ -68,8 +68,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.225.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -109,9 +109,9 @@ GEM
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-firebase_app_distribution (0.10.0)
|
||||
fastlane-plugin-firebase_app_distribution (0.9.1)
|
||||
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
|
||||
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
@@ -158,12 +158,12 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
http-cookie (1.0.7)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
jwt (2.10.1)
|
||||
json (2.8.1)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
@@ -174,7 +174,7 @@ GEM
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
@@ -182,10 +182,10 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rouge (3.28.0)
|
||||
rexml (3.3.9)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
@@ -216,8 +216,8 @@ GEM
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 35
|
||||
- **Target SDK**: 34
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
@@ -132,11 +132,6 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- https://github.com/firebase/firebase-android-sdk
|
||||
- Purpose: SDK for crash and non-fatal error reporting. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Google Play Reviews**
|
||||
- https://developer.android.com/reference/com/google/android/play/core/release-notes
|
||||
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Glide**
|
||||
- https://github.com/bumptech/glide
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.utils.cxx.io.removeExtensionIfPresent
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFileIdTask
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
|
||||
import com.google.gms.googleservices.GoogleServicesTask
|
||||
import dagger.hilt.android.plugin.util.capitalize
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
@@ -35,16 +32,6 @@ val userProperties = Properties().apply {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads CI-specific build properties that are not checked into source control.
|
||||
*/
|
||||
val ciProperties = Properties().apply {
|
||||
val ciPropsFile = File(rootDir, "ci.properties")
|
||||
if (ciPropsFile.exists()) {
|
||||
FileInputStream(ciPropsFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.x8bit.bitwarden"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
@@ -64,12 +51,6 @@ android {
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
|
||||
)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
@@ -134,39 +115,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val bundlesDir = "${layout.buildDirectory.get()}/outputs/bundle"
|
||||
outputs
|
||||
.mapNotNull { it as? BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val fileNameWithoutExtension = when (flavorName) {
|
||||
"fdroid" -> "$applicationId-$flavorName"
|
||||
"standard" -> "$applicationId"
|
||||
else -> output.outputFileName.removeExtensionIfPresent(".apk")
|
||||
}
|
||||
|
||||
// Set the APK output filename.
|
||||
output.outputFileName = "$fileNameWithoutExtension.apk"
|
||||
|
||||
val variantName = name
|
||||
val renameTaskName = "rename${variantName.capitalize()}AabFiles"
|
||||
tasks.register(renameTaskName) {
|
||||
group = "build"
|
||||
description = "Renames the bundle files for $variantName variant"
|
||||
doLast {
|
||||
renameFile(
|
||||
"$bundlesDir/$variantName/$namespace-$flavorName-${buildType.name}.aab",
|
||||
"$fileNameWithoutExtension.aab",
|
||||
)
|
||||
}
|
||||
}
|
||||
// Force renaming task to execute after the variant is built.
|
||||
tasks
|
||||
.getByName("bundle${variantName.capitalize()}")
|
||||
.finalizedBy(renameTaskName)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility(libs.versions.jvmTarget.get())
|
||||
targetCompatibility(libs.versions.jvmTarget.get())
|
||||
@@ -268,7 +216,6 @@ dependencies {
|
||||
standardImplementation(libs.google.firebase.cloud.messaging)
|
||||
standardImplementation(platform(libs.google.firebase.bom))
|
||||
standardImplementation(libs.google.firebase.crashlytics)
|
||||
standardImplementation(libs.google.play.review)
|
||||
|
||||
testImplementation(libs.androidx.compose.ui.test)
|
||||
testImplementation(libs.google.hilt.android.testing)
|
||||
@@ -325,7 +272,6 @@ kover {
|
||||
"*_*Factory\$*",
|
||||
"*.Hilt_*",
|
||||
"*_HiltModules",
|
||||
"*_HiltModules*",
|
||||
"*_HiltModules\$*",
|
||||
"*_Impl",
|
||||
"*_Impl\$*",
|
||||
@@ -352,10 +298,6 @@ tasks {
|
||||
dependsOn("detekt")
|
||||
}
|
||||
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
}
|
||||
|
||||
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
jvmTarget = libs.versions.jvmTarget.get()
|
||||
}
|
||||
@@ -368,16 +310,15 @@ tasks {
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
|
||||
android.sourceSets["main"].res.srcDirs("src/test/res")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
// Disable Fdroid-specific tasks that we want to exclude
|
||||
val fdroidTasksToDisable = tasks.withType<GoogleServicesTask>() +
|
||||
val tasks = tasks.withType<GoogleServicesTask>() +
|
||||
tasks.withType<InjectMappingFileIdTask>() +
|
||||
tasks.withType<UploadMappingFileTask>()
|
||||
fdroidTasksToDisable
|
||||
tasks
|
||||
.filter { it.name.contains("Fdroid") }
|
||||
.forEach { it.enabled = false }
|
||||
}
|
||||
@@ -394,17 +335,8 @@ sonar {
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameFile(path: String, newName: String) {
|
||||
val originalFile = File(path)
|
||||
if (!originalFile.exists()) {
|
||||
println("File $originalFile does not exist!")
|
||||
return
|
||||
}
|
||||
|
||||
val newFile = File(originalFile.parentFile, newName)
|
||||
if (originalFile.renameTo(newFile)) {
|
||||
println("Renamed $originalFile to $newFile")
|
||||
} else {
|
||||
throw RuntimeException("Failed to rename $originalFile to $newFile")
|
||||
tasks {
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "ee158c483edfe5102504670f3d9845d4",
|
||||
"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, `manage` INTEGER, 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
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"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, 'ee158c483edfe5102504670f3d9845d4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.manager.review
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* No-op implementation of [AppReviewManager] for F-Droid builds.
|
||||
*/
|
||||
class AppReviewManagerImpl(
|
||||
activity: Activity,
|
||||
) : AppReviewManager {
|
||||
override fun promptForReview() = Unit
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
|
||||
|
||||
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
|
||||
|
||||
Note that each build type uses a different value for knownCerts.
|
||||
@@ -320,11 +320,6 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
<!-- To Query Chrome Beta: -->
|
||||
<package android:name="com.chrome.beta" />
|
||||
|
||||
<!-- To Query Chrome Stable: -->
|
||||
<package android:name="com.android.chrome" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "net.quetta.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
@@ -50,30 +24,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.ironfoxoss.ironfox",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fenix",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
@@ -85,6 +35,46 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ 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.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -11,16 +11,15 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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 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.manager.util.ObserveScreenDataEffect
|
||||
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
|
||||
@@ -40,6 +39,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityActivityManager: AccessibilityActivityManager
|
||||
|
||||
@Inject
|
||||
lateinit var autofillActivityManager: AutofillActivityManager
|
||||
|
||||
@@ -55,7 +57,6 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
@@ -69,14 +70,13 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// Within the app the language and theme will change dynamically and will be managed by the
|
||||
// OS, but we need to ensure we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
// Within the app the language will change dynamically and will be managed
|
||||
// by the OS, but we need to ensure we properly set the language when
|
||||
// upgrading from older versions that handle this differently.
|
||||
settingsRepository.appLanguage.localeName?.let { localeName ->
|
||||
val localeList = LocaleListCompat.forLanguageTags(localeName)
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
@@ -98,29 +98,10 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
)
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppTheme -> {
|
||||
AppCompatDelegate.setDefaultNightMode(event.osTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
|
||||
@@ -13,15 +13,13 @@ 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.getFido2CreateCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
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.AppResumeScreenData
|
||||
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
|
||||
@@ -73,7 +71,6 @@ class MainViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
@@ -111,11 +108,6 @@ class MainViewModel @Inject constructor(
|
||||
.appThemeStateFlow
|
||||
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
settingsRepository
|
||||
.appLanguageStateFlow
|
||||
.map { MainEvent.UpdateAppLocale(it.localeName) }
|
||||
.onEach(::sendEvent)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.isScreenCaptureAllowedStateFlow
|
||||
@@ -188,14 +180,6 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
else -> appResumeManager.setResumeScreen(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +211,6 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update { it.copy(theme = action.theme) }
|
||||
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
|
||||
}
|
||||
|
||||
private fun handleVaultUnlockStateChange() {
|
||||
@@ -274,7 +257,7 @@ class MainViewModel @Inject constructor(
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
@@ -335,31 +318,25 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fido2CreateCredentialRequestData != null -> {
|
||||
fido2CredentialRequestData != null -> {
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CreateCredentialRequestData.isUserVerified
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
|
||||
fido2CredentialRequest = fido2CredentialRequestData,
|
||||
)
|
||||
|
||||
// Switch accounts if the selected user is not the active user.
|
||||
if (authRepository.activeUserId != null &&
|
||||
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
|
||||
authRepository.activeUserId != fido2CredentialRequestData.userId
|
||||
) {
|
||||
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
|
||||
authRepository.switchAccount(fido2CredentialRequestData.userId)
|
||||
}
|
||||
}
|
||||
|
||||
fido2CredentialAssertionRequest != null -> {
|
||||
// If device biometric verification was performed as part of single-tap
|
||||
// authentication, set the user's verification state to the device result.
|
||||
// Otherwise, retain the verification state as-is.
|
||||
fido2CredentialAssertionRequest.isUserVerified
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = fido2CredentialAssertionRequest,
|
||||
@@ -466,11 +443,6 @@ sealed class MainAction {
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to save the app resume screen
|
||||
*/
|
||||
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
@@ -546,18 +518,4 @@ sealed class MainEvent {
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
data class UpdateAppLocale(
|
||||
val localeName: String?,
|
||||
) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app theme has been updated.
|
||||
*/
|
||||
data class UpdateAppTheme(
|
||||
val osTheme: Int,
|
||||
) : MainEvent()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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.NewDeviceNoticeState
|
||||
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
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary access point for disk information.
|
||||
@@ -173,16 +171,6 @@ interface AuthDiskSource {
|
||||
pendingAuthRequest: PendingAuthRequestJson?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the biometrics initialization vector for the given [userId].
|
||||
*/
|
||||
fun getUserBiometricInitVector(userId: String): ByteArray?
|
||||
|
||||
/**
|
||||
* Stores the biometrics initialization vector for the given [userId].
|
||||
*/
|
||||
fun storeUserBiometricInitVector(userId: String, iv: ByteArray?)
|
||||
|
||||
/**
|
||||
* Gets the biometrics key for the given [userId].
|
||||
*/
|
||||
@@ -340,27 +328,7 @@ interface AuthDiskSource {
|
||||
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value.
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
|
||||
*/
|
||||
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the new device notice state for the given [userId].
|
||||
*/
|
||||
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
|
||||
|
||||
/**
|
||||
* Stores the new device notice state for the given [userId].
|
||||
*/
|
||||
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
|
||||
|
||||
/**
|
||||
* Gets the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun getLastLockTimestamp(userId: String): Instant?
|
||||
|
||||
/**
|
||||
* Stores the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)
|
||||
}
|
||||
|
||||
@@ -2,8 +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.NewDeviceNoticeDisplayStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
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
|
||||
@@ -15,15 +13,14 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
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_INIT_VECTOR_KEY = "biometricInitializationVector"
|
||||
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
|
||||
private const val DEVICE_KEY_KEY = "deviceKey"
|
||||
@@ -49,8 +46,6 @@ 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"
|
||||
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
|
||||
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -147,7 +142,6 @@ class AuthDiskSourceImpl(
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
storeOrganizations(userId = userId, organizations = null)
|
||||
storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
storeMasterPasswordHash(userId = userId, passwordHash = null)
|
||||
storePolicies(userId = userId, policies = null)
|
||||
@@ -156,7 +150,6 @@ class AuthDiskSourceImpl(
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
@@ -284,17 +277,6 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getUserBiometricInitVector(userId: String): ByteArray? =
|
||||
getEncryptedString(key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId))
|
||||
?.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
override fun storeUserBiometricInitVector(userId: String, iv: ByteArray?) {
|
||||
putEncryptedString(
|
||||
key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId),
|
||||
value = iv?.toString(Charsets.ISO_8859_1),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getUserBiometricUnlockKey(userId: String): String? =
|
||||
getEncryptedString(key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId))
|
||||
|
||||
@@ -489,35 +471,6 @@ class AuthDiskSourceImpl(
|
||||
getMutableShowImportLoginsFlow(userId)
|
||||
.onSubscription { emit(getShowImportLogins(userId)) }
|
||||
|
||||
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
|
||||
return getString(key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId))?.let {
|
||||
json.decodeFromStringOrNull(it)
|
||||
} ?: NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
|
||||
putString(
|
||||
key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId),
|
||||
value = newState?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLastLockTimestamp(userId: String): Instant? {
|
||||
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
|
||||
putLong(
|
||||
key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId),
|
||||
value = lastLockTimestamp?.toEpochMilli(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
||||
@@ -2,12 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Represents the current account information for a given user.
|
||||
@@ -37,7 +33,6 @@ data class AccountJson(
|
||||
* @property userId The ID of the user.
|
||||
* @property email The user's email address.
|
||||
* @property isEmailVerified Whether or not the user's email is verified.
|
||||
* @property isTwoFactorEnabled If the profile has two factor authentication enabled.
|
||||
* @property name The user's name (if applicable).
|
||||
* @property stamp The account's security stamp (if applicable).
|
||||
* @property organizationId The ID of the associated organization (if applicable).
|
||||
@@ -49,9 +44,7 @@ data class AccountJson(
|
||||
* @property kdfMemory The amount of memory to use when calculating a password hash (MB).
|
||||
* @property kdfParallelism The number of threads to use when calculating a password hash.
|
||||
* @property userDecryptionOptions The options available to a user for decryption.
|
||||
* @property creationDate The creation date of the account.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class Profile(
|
||||
@SerialName("userId")
|
||||
@@ -63,9 +56,6 @@ data class AccountJson(
|
||||
@SerialName("emailVerified")
|
||||
val isEmailVerified: Boolean?,
|
||||
|
||||
@SerialName("isTwoFactorEnabled")
|
||||
val isTwoFactorEnabled: Boolean?,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@@ -96,13 +86,8 @@ data class AccountJson(
|
||||
@SerialName("kdfParallelism")
|
||||
val kdfParallelism: Int?,
|
||||
|
||||
@SerialName("userDecryptionOptions")
|
||||
@JsonNames("accountDecryptionOptions")
|
||||
@SerialName("accountDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("creationDate")
|
||||
@Contextual
|
||||
val creationDate: ZonedDateTime?,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable
|
||||
* Represents URLs for various Bitwarden domains.
|
||||
*
|
||||
* @property base The overall base URL.
|
||||
* @property keyUri A Uri containing the alias and host of the key used for mutual TLS.
|
||||
* @property api Separate base URL for the "/api" domain (if applicable).
|
||||
* @property identity Separate base URL for the "/identity" domain (if applicable).
|
||||
* @property icon Separate base URL for the icon domain (if applicable).
|
||||
@@ -20,9 +19,6 @@ data class EnvironmentUrlDataJson(
|
||||
@SerialName("base")
|
||||
val base: String,
|
||||
|
||||
@SerialName("keyUri")
|
||||
val keyUri: String? = null,
|
||||
|
||||
@SerialName("api")
|
||||
val api: String? = null,
|
||||
|
||||
@@ -55,7 +51,6 @@ data class EnvironmentUrlDataJson(
|
||||
*/
|
||||
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
keyUri = null,
|
||||
api = "https://api.bitwarden.com",
|
||||
identity = "https://identity.bitwarden.com",
|
||||
icon = "https://icons.bitwarden.net",
|
||||
@@ -76,7 +71,6 @@ data class EnvironmentUrlDataJson(
|
||||
*/
|
||||
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.eu",
|
||||
keyUri = null,
|
||||
api = "https://api.bitwarden.eu",
|
||||
identity = "https://identity.bitwarden.eu",
|
||||
icon = "https://icons.bitwarden.eu",
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Describes the current display status of the new device notice screen.
|
||||
*/
|
||||
@Serializable
|
||||
enum class NewDeviceNoticeDisplayStatus {
|
||||
/**
|
||||
* The user has seen the screen and indicated they can access their email.
|
||||
*/
|
||||
@SerialName("canAccessEmail")
|
||||
CAN_ACCESS_EMAIL,
|
||||
|
||||
/**
|
||||
* The user has indicated they can access their email
|
||||
* as specified by the Permanent mode of the notice.
|
||||
*/
|
||||
@SerialName("canAccessEmailPermanent")
|
||||
CAN_ACCESS_EMAIL_PERMANENT,
|
||||
|
||||
/**
|
||||
* The user has not seen the screen.
|
||||
*/
|
||||
@SerialName("hasNotSeen")
|
||||
HAS_NOT_SEEN,
|
||||
|
||||
/**
|
||||
* The user has seen the screen and selected "remind me later".
|
||||
*/
|
||||
@SerialName("hasSeen")
|
||||
HAS_SEEN,
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the new device notice screen.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class NewDeviceNoticeState(
|
||||
@SerialName("displayStatus")
|
||||
val displayStatus: NewDeviceNoticeDisplayStatus,
|
||||
|
||||
@SerialName("lastSeenDate")
|
||||
@Contextual
|
||||
val lastSeenDate: ZonedDateTime?,
|
||||
) {
|
||||
/**
|
||||
* Whether the [lastSeenDate] is at least 7 days old.
|
||||
*/
|
||||
val shouldDisplayNoticeIfSeen = lastSeenDate
|
||||
?.isBefore(
|
||||
ZonedDateTime.now().minusDays(7),
|
||||
)
|
||||
?: false
|
||||
}
|
||||
@@ -3,7 +3,6 @@ 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.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
@@ -29,9 +28,4 @@ interface UnauthenticatedAccountsApi {
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/resend-new-device-otp")
|
||||
suspend fun resendNewDeviceOtp(
|
||||
@Body body: ResendNewDeviceOtpRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -47,13 +47,12 @@ interface UnauthenticatedIdentityApi {
|
||||
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
|
||||
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
|
||||
@Field(value = "authRequest") authRequestId: String?,
|
||||
@Field(value = "newDeviceOtp") newDeviceOtp: String?,
|
||||
): NetworkResult<GetTokenResponseJson.Success>
|
||||
|
||||
@GET("/sso/prevalidate")
|
||||
suspend fun prevalidateSso(
|
||||
@Query("domainHint") organizationIdentifier: String,
|
||||
): NetworkResult<PrevalidateSsoResponseJson.Success>
|
||||
): NetworkResult<PrevalidateSsoResponseJson>
|
||||
|
||||
/**
|
||||
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
|
||||
|
||||
@@ -21,7 +21,5 @@ enum class AuthRequestTypeJson {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class AuthRequestTypeSerializer : BaseEnumeratedIntSerializer<AuthRequestTypeJson>(
|
||||
className = "AuthRequestTypeJson",
|
||||
values = AuthRequestTypeJson.entries.toTypedArray(),
|
||||
)
|
||||
private class AuthRequestTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<AuthRequestTypeJson>(AuthRequestTypeJson.entries.toTypedArray())
|
||||
|
||||
@@ -107,28 +107,6 @@ sealed class GetTokenResponseJson {
|
||||
val errorMessage: String?
|
||||
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
|
||||
|
||||
/**
|
||||
* The type of invalid responses that can be received.
|
||||
*/
|
||||
sealed class InvalidType {
|
||||
/**
|
||||
* Represents an invalid response indicating that a new device verification is required.
|
||||
*/
|
||||
data object NewDeviceVerification : InvalidType()
|
||||
|
||||
/**
|
||||
* Represents generic invalid response
|
||||
*/
|
||||
data object GenericInvalid : InvalidType()
|
||||
}
|
||||
|
||||
val invalidType: InvalidType
|
||||
get() = if (errorMessage?.lowercase() == "new device verification required") {
|
||||
InvalidType.NewDeviceVerification
|
||||
} else {
|
||||
InvalidType.GenericInvalid
|
||||
}
|
||||
|
||||
/**
|
||||
* The error body of an invalid request containing a message.
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,5 @@ enum class KdfTypeJson {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class KdfTypeSerializer : BaseEnumeratedIntSerializer<KdfTypeJson>(
|
||||
className = "KdfTypeJson",
|
||||
values = KdfTypeJson.entries.toTypedArray(),
|
||||
)
|
||||
private class KdfTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<KdfTypeJson>(KdfTypeJson.entries.toTypedArray())
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* Decryption options related to a user's key connector.
|
||||
*
|
||||
* @property keyConnectorUrl URL to the user's key connector.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class KeyConnectorUserDecryptionOptionsJson(
|
||||
@SerialName("keyConnectorUrl")
|
||||
@JsonNames("KeyConnectorUrl")
|
||||
@SerialName("KeyConnectorUrl")
|
||||
val keyConnectorUrl: String,
|
||||
)
|
||||
|
||||
@@ -7,20 +7,6 @@ import kotlinx.serialization.Serializable
|
||||
* Response body from the SSO prevalidate request.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class PrevalidateSsoResponseJson {
|
||||
/**
|
||||
* Models json body of a successful response.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@SerialName("token") val token: String?,
|
||||
) : PrevalidateSsoResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of an error response.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("message") val message: String?,
|
||||
) : PrevalidateSsoResponseJson()
|
||||
}
|
||||
data class PrevalidateSsoResponseJson(
|
||||
@SerialName("token") val token: String?,
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Hold the information necessary to resend the email with the
|
||||
* new device verification code.
|
||||
*
|
||||
* @property email The user's email address.
|
||||
* @property passwordHash The master password hash
|
||||
*/
|
||||
@Serializable
|
||||
data class ResendNewDeviceOtpRequestJson(
|
||||
@SerialName("Email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("MasterPasswordHash")
|
||||
val passwordHash: String?,
|
||||
)
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* Decryption options related to a user's trusted device.
|
||||
@@ -15,26 +13,20 @@ import kotlinx.serialization.json.JsonNames
|
||||
* @property hasManageResetPasswordPermission Whether or not the user has manage reset password
|
||||
* permission.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class TrustedDeviceUserDecryptionOptionsJson(
|
||||
@SerialName("encryptedPrivateKey")
|
||||
@JsonNames("EncryptedPrivateKey")
|
||||
@SerialName("EncryptedPrivateKey")
|
||||
val encryptedPrivateKey: String?,
|
||||
|
||||
@SerialName("encryptedUserKey")
|
||||
@JsonNames("EncryptedUserKey")
|
||||
@SerialName("EncryptedUserKey")
|
||||
val encryptedUserKey: String?,
|
||||
|
||||
@SerialName("hasAdminApproval")
|
||||
@JsonNames("HasAdminApproval")
|
||||
@SerialName("HasAdminApproval")
|
||||
val hasAdminApproval: Boolean,
|
||||
|
||||
@SerialName("hasLoginApprovingDevice")
|
||||
@JsonNames("HasLoginApprovingDevice")
|
||||
@SerialName("HasLoginApprovingDevice")
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
|
||||
@SerialName("hasManageResetPasswordPermission")
|
||||
@JsonNames("HasManageResetPasswordPermission")
|
||||
@SerialName("HasManageResetPasswordPermission")
|
||||
val hasManageResetPasswordPermission: Boolean,
|
||||
)
|
||||
|
||||
@@ -39,7 +39,5 @@ enum class TwoFactorAuthMethod(val value: UInt) {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class TwoFactorAuthMethodSerializer : BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(
|
||||
className = "TwoFactorAuthMethod",
|
||||
values = TwoFactorAuthMethod.entries.toTypedArray(),
|
||||
)
|
||||
private class TwoFactorAuthMethodSerializer :
|
||||
BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(TwoFactorAuthMethod.entries.toTypedArray())
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* The options available to a user for decryption.
|
||||
@@ -14,18 +12,14 @@ import kotlinx.serialization.json.JsonNames
|
||||
* device.
|
||||
* @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class UserDecryptionOptionsJson(
|
||||
@SerialName("hasMasterPassword")
|
||||
@JsonNames("HasMasterPassword")
|
||||
@SerialName("HasMasterPassword")
|
||||
val hasMasterPassword: Boolean,
|
||||
|
||||
@SerialName("trustedDeviceOption")
|
||||
@JsonNames("TrustedDeviceOption")
|
||||
@SerialName("TrustedDeviceOption")
|
||||
val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("keyConnectorOption")
|
||||
@JsonNames("KeyConnectorOption")
|
||||
@SerialName("KeyConnectorOption")
|
||||
val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyReq
|
||||
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.ResendNewDeviceOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
|
||||
@@ -53,11 +52,6 @@ interface AccountsService {
|
||||
*/
|
||||
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Resend the email with the verification code for new devices
|
||||
*/
|
||||
suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Reset the password.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMaster
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
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
|
||||
@@ -115,11 +114,6 @@ class AccountsServiceImpl(
|
||||
.resendVerificationCodeEmail(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit> =
|
||||
unauthenticatedAccountsApi
|
||||
.resendNewDeviceOtp(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||
if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi
|
||||
|
||||
@@ -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.SendVerificationEmailResponseJson
|
||||
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
|
||||
@@ -46,7 +45,6 @@ interface IdentityService {
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
newDeviceOtp: String? = null,
|
||||
): Result<GetTokenResponseJson>
|
||||
|
||||
/**
|
||||
@@ -70,7 +68,7 @@ interface IdentityService {
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson>
|
||||
): Result<String?>
|
||||
|
||||
/**
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
|
||||
@@ -11,7 +11,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.SendVerificationEmailResponseJson
|
||||
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
|
||||
@@ -60,7 +59,6 @@ class IdentityServiceImpl(
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
newDeviceOtp: String?,
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
@@ -80,28 +78,22 @@ class IdentityServiceImpl(
|
||||
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
|
||||
captchaResponse = captchaToken,
|
||||
authRequestId = authModel.authRequestId,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun prevalidateSso(
|
||||
organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
|
||||
@@ -109,15 +101,6 @@ class IdentityServiceImpl(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<PrevalidateSsoResponseJson.Error>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
@@ -147,26 +130,15 @@ class IdentityServiceImpl(
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson> {
|
||||
): Result<String?> {
|
||||
return unauthenticatedIdentityApi
|
||||
.sendVerificationEmail(body = body)
|
||||
.toResult()
|
||||
.map { SendVerificationEmailResponseJson.Success(it?.content) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
.map { it?.content }
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.AuthClient
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
@@ -17,7 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
|
||||
* [AuthClient].
|
||||
* [ClientAuth].
|
||||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
sdkClientManager: SdkClientManager,
|
||||
|
||||
@@ -55,5 +55,5 @@ class TrustedDeviceManagerImpl(
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
}
|
||||
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
|
||||
.map { }
|
||||
.map { Unit }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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.NewDeviceNoticeState
|
||||
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
|
||||
@@ -230,19 +229,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
organizationIdentifier: String,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Repeat the previous login attempt but this time with New Device OTP
|
||||
* information. Password is included if available to unlock the vault after
|
||||
* authentication. Updated access token will be reflected in [authStateFlow].
|
||||
*/
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Log out the current user.
|
||||
*/
|
||||
@@ -265,11 +251,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
suspend fun resendVerificationCodeEmail(): ResendEmailResult
|
||||
|
||||
/**
|
||||
* Resend the email with the new device verification code.
|
||||
*/
|
||||
suspend fun resendNewDeviceOtp(): ResendEmailResult
|
||||
|
||||
/**
|
||||
* Switches to the account corresponding to the given [userId] if possible.
|
||||
*/
|
||||
@@ -380,10 +361,8 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
|
||||
/**
|
||||
* Get the password strength for the given [email] and [password] combo.
|
||||
* If no value is passed for the [email] will use the active email of the current active
|
||||
* account via the [userStateFlow].
|
||||
*/
|
||||
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
|
||||
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
|
||||
|
||||
/**
|
||||
* Validates the master password for the current logged in user.
|
||||
@@ -421,20 +400,5 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
/**
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(status: OnboardingStatus)
|
||||
|
||||
/**
|
||||
* Checks if a new device notice should be displayed.
|
||||
*/
|
||||
fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean
|
||||
|
||||
/**
|
||||
* Gets the new device notice state of active user.
|
||||
*/
|
||||
fun getNewDeviceNoticeState(): NewDeviceNoticeState?
|
||||
|
||||
/**
|
||||
* Stores the new device notice state for active user.
|
||||
*/
|
||||
fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?)
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
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.NewDeviceNoticeDisplayStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
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
|
||||
@@ -17,16 +16,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
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.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
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.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
@@ -98,7 +94,6 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
|
||||
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.datasource.network.util.isSslHandShakeError
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
@@ -110,7 +105,6 @@ 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.model.Environment
|
||||
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
|
||||
@@ -120,6 +114,7 @@ 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
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
@@ -146,7 +141,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -226,11 +220,6 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private var resendEmailRequestJson: ResendEmailRequestJson? = null
|
||||
|
||||
/**
|
||||
* The information necessary to resend the verification code email for new devices.
|
||||
*/
|
||||
private var resendNewDeviceOtpRequestJson: ResendNewDeviceOtpRequestJson? = null
|
||||
|
||||
private var organizationIdentifier: String? = null
|
||||
|
||||
/**
|
||||
@@ -636,12 +625,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { throwable ->
|
||||
when {
|
||||
throwable.isSslHandShakeError() -> LoginResult.CertificateError
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
onFailure = { LoginResult.Error(errorMessage = null) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
|
||||
@@ -690,26 +674,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
loginCommon(
|
||||
email = email,
|
||||
password = password,
|
||||
authModel = it,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
ssoCode: String,
|
||||
@@ -791,16 +755,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
|
||||
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
|
||||
resendNewDeviceOtpRequestJson
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
|
||||
override fun switchAccount(userId: String): SwitchAccountResult {
|
||||
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
|
||||
val previousActiveUserId = currentUserState.activeUserId
|
||||
@@ -1125,7 +1079,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
@@ -1200,21 +1153,13 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is PrevalidateSsoResponseJson.Error -> {
|
||||
PrevalidateSsoResult.Failure(message = it.message)
|
||||
}
|
||||
|
||||
is PrevalidateSsoResponseJson.Success -> {
|
||||
if (it.token.isNullOrBlank()) {
|
||||
PrevalidateSsoResult.Failure()
|
||||
} else {
|
||||
PrevalidateSsoResult.Success(token = it.token)
|
||||
}
|
||||
}
|
||||
if (it.token.isNullOrBlank()) {
|
||||
PrevalidateSsoResult.Failure
|
||||
} else {
|
||||
PrevalidateSsoResult.Success(it.token)
|
||||
}
|
||||
},
|
||||
onFailure = { PrevalidateSsoResult.Failure() },
|
||||
onFailure = { PrevalidateSsoResult.Failure },
|
||||
)
|
||||
|
||||
override fun setSsoCallbackResult(result: SsoCallbackResult) {
|
||||
@@ -1241,17 +1186,12 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
override suspend fun getPasswordStrength(
|
||||
email: String?,
|
||||
email: String,
|
||||
password: String,
|
||||
): PasswordStrengthResult =
|
||||
authSdkSource
|
||||
.passwordStrength(
|
||||
email = email
|
||||
?: userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.email
|
||||
.orEmpty(),
|
||||
email = email,
|
||||
password = password,
|
||||
)
|
||||
.fold(
|
||||
@@ -1309,17 +1249,41 @@ class AuthRepositoryImpl(
|
||||
?.activeAccount
|
||||
?.profile
|
||||
?: return ValidatePinResult.Error
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
val pinProtectedUserKey = authDiskSource
|
||||
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
|
||||
// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead
|
||||
// try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise
|
||||
// the PIN is incorrect.
|
||||
return vaultSdkSource
|
||||
.validatePin(
|
||||
.initializeCrypto(
|
||||
userId = activeAccount.userId,
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = activeAccount.toSdkParams(),
|
||||
email = activeAccount.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.Pin(
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ValidatePinResult.Success(isValid = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
InitializeCryptoResult.Success -> {
|
||||
ValidatePinResult.Success(isValid = true)
|
||||
}
|
||||
|
||||
is InitializeCryptoResult.AuthenticationError -> {
|
||||
ValidatePinResult.Success(isValid = false)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { ValidatePinResult.Error },
|
||||
)
|
||||
}
|
||||
@@ -1344,15 +1308,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is SendVerificationEmailResponseJson.Invalid -> {
|
||||
SendVerificationEmailResult.Error(it.message)
|
||||
}
|
||||
|
||||
is SendVerificationEmailResponseJson.Success -> {
|
||||
SendVerificationEmailResult.Success(it.emailVerificationToken)
|
||||
}
|
||||
}
|
||||
SendVerificationEmailResult.Success(it)
|
||||
},
|
||||
onFailure = {
|
||||
SendVerificationEmailResult.Error(null)
|
||||
@@ -1384,99 +1340,8 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setOnboardingStatus(status: OnboardingStatus) {
|
||||
activeUserId?.let { userId ->
|
||||
authDiskSource.storeOnboardingStatus(
|
||||
userId = userId,
|
||||
onboardingStatus = status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
|
||||
return activeUserId?.let { userId ->
|
||||
authDiskSource.getNewDeviceNoticeState(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?) {
|
||||
activeUserId?.let { userId ->
|
||||
authDiskSource.storeNewDeviceNoticeState(userId = userId, newState = newState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean {
|
||||
return activeUserId?.let { userId ->
|
||||
val temporaryFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
val permanentFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
|
||||
// check if feature flags are disabled
|
||||
if (!temporaryFlag && !permanentFlag) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!newDeviceNoticePreConditionsValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val newDeviceNoticeState = authDiskSource.getNewDeviceNoticeState(userId = userId)
|
||||
return when (newDeviceNoticeState.displayStatus) {
|
||||
// if the user has already attested email access but permanent flag is enabled,
|
||||
// the notice needs to appear again
|
||||
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL -> permanentFlag
|
||||
// if the user has already seen but 7 days have already passed,
|
||||
// the notice needs to appear again
|
||||
NewDeviceNoticeDisplayStatus.HAS_SEEN ->
|
||||
newDeviceNoticeState.shouldDisplayNoticeIfSeen
|
||||
|
||||
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
|
||||
// the user never needs to see the notice again
|
||||
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
|
||||
}
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the preconditions are met for a user to see a new device notice:
|
||||
* - Must be a Bitwarden cloud user.
|
||||
* - The account must be at least one week old.
|
||||
* - Cannot have an active policy requiring SSO to be enabled.
|
||||
* - Cannot have two-factor authentication enabled.
|
||||
*/
|
||||
private fun newDeviceNoticePreConditionsValid(): Boolean {
|
||||
val checkEnvironment = !featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
|
||||
val isSelfHosted = environmentRepository.environment.type == Environment.Type.SELF_HOSTED
|
||||
if (checkEnvironment && isSelfHosted) {
|
||||
return false
|
||||
}
|
||||
|
||||
val userProfile = authDiskSource.userState?.activeAccount?.profile
|
||||
val isProfileAtLeastWeekOld = userProfile
|
||||
?.let {
|
||||
it.creationDate
|
||||
?.plusWeeks(1)
|
||||
?.isBefore(
|
||||
ZonedDateTime.now(),
|
||||
)
|
||||
}
|
||||
?: false
|
||||
if (!isProfileAtLeastWeekOld) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hasTwoFactorEnabled = userProfile
|
||||
?.isTwoFactorEnabled
|
||||
?: false
|
||||
if (hasTwoFactorEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hasSSOPolicy =
|
||||
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
|
||||
.any { p -> p.isEnabled }
|
||||
|
||||
return !hasSSOPolicy
|
||||
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@@ -1634,7 +1499,6 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
uniqueAppId = authDiskSource.uniqueAppId,
|
||||
@@ -1642,16 +1506,11 @@ class AuthRepositoryImpl(
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { throwable ->
|
||||
when {
|
||||
throwable.isSslHandShakeError() -> LoginResult.CertificateError
|
||||
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
|
||||
LoginResult.UnofficialServerError
|
||||
}
|
||||
|
||||
onFailure = {
|
||||
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
|
||||
false -> LoginResult.UnofficialServerError
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
@@ -1676,22 +1535,9 @@ class AuthRepositoryImpl(
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid -> {
|
||||
when (loginResponse.invalidType) {
|
||||
is GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification ->
|
||||
handleLoginCommonNewDeviceVerification(
|
||||
email = email,
|
||||
authModel = authModel,
|
||||
error = loginResponse.errorMessage,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
|
||||
LoginResult.Error(
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GetTokenResponseJson.Invalid -> LoginResult.Error(
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1779,6 +1625,15 @@ 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
|
||||
@@ -1807,7 +1662,6 @@ class AuthRepositoryImpl(
|
||||
twoFactorResponse = null
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
resendNewDeviceOtpRequestJson = null
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
@@ -1840,24 +1694,6 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method that processes the
|
||||
* [GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification] when logging in.
|
||||
*/
|
||||
private fun handleLoginCommonNewDeviceVerification(
|
||||
email: String,
|
||||
authModel: IdentityTokenAuthModel,
|
||||
error: String?,
|
||||
): LoginResult {
|
||||
identityTokenAuthModel = authModel
|
||||
resendNewDeviceOtpRequestJson = ResendNewDeviceOtpRequestJson(
|
||||
email = email,
|
||||
passwordHash = authModel.password,
|
||||
)
|
||||
|
||||
return LoginResult.NewDeviceVerification(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with key connector data.
|
||||
*/
|
||||
|
||||
@@ -28,14 +28,4 @@ sealed class LoginResult {
|
||||
* There was an error while logging into an unofficial Bitwarden server.
|
||||
*/
|
||||
data object UnofficialServerError : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error in validating the certificate chain for the server
|
||||
*/
|
||||
data object CertificateError : LoginResult()
|
||||
|
||||
/**
|
||||
* New device verification is required
|
||||
*/
|
||||
data class NewDeviceVerification(val errorMessage: String?) : LoginResult()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
|
||||
@@ -14,7 +14,5 @@ sealed class PrevalidateSsoResult {
|
||||
/**
|
||||
* There was an error in prevalidation.
|
||||
*/
|
||||
data class Failure(
|
||||
val message: String? = null,
|
||||
) : PrevalidateSsoResult()
|
||||
data object Failure : PrevalidateSsoResult()
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ fun GetTokenResponseJson.Success.toUserState(
|
||||
userId = userId,
|
||||
email = jwtTokenData.email,
|
||||
isEmailVerified = jwtTokenData.isEmailVerified,
|
||||
isTwoFactorEnabled = null,
|
||||
name = jwtTokenData.name,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
@@ -37,7 +36,6 @@ fun GetTokenResponseJson.Success.toUserState(
|
||||
kdfMemory = this.kdfMemory,
|
||||
kdfParallelism = this.kdfParallelism,
|
||||
userDecryptionOptions = this.userDecryptionOptions,
|
||||
creationDate = null,
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = environmentUrlData,
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Internal, generally basic [Json] instance for JWT parsing purposes.
|
||||
@@ -18,24 +17,17 @@ private val json: Json by lazy {
|
||||
/**
|
||||
* Parses a [JwtTokenDataJson] from the given [jwtToken], or `null` if this parsing is not possible.
|
||||
*/
|
||||
@Suppress("MagicNumber", "TooGenericExceptionCaught")
|
||||
@Suppress("MagicNumber")
|
||||
fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? {
|
||||
val parts = jwtToken.split(".")
|
||||
if (parts.size != 3) {
|
||||
Timber.e(IllegalArgumentException("Incorrect number of parts"), "Invalid JWT Token")
|
||||
return null
|
||||
}
|
||||
if (parts.size != 3) return null
|
||||
|
||||
val dataJson = parts[1]
|
||||
val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: run {
|
||||
Timber.e(IllegalArgumentException("Unable to decode"), "Invalid JWT Token")
|
||||
return null
|
||||
}
|
||||
val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: return null
|
||||
|
||||
return try {
|
||||
json.decodeFromString<JwtTokenDataJson>(decodedDataJson)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "Failed to decode JwtTokenDataJson")
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,6 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
avatarColorHex = syncProfile.avatarColor,
|
||||
stamp = syncProfile.securityStamp,
|
||||
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.PowerManager
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
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
|
||||
@@ -56,12 +55,8 @@ object AccessibilityModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityEnabledManager(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
): AccessibilityEnabledManager =
|
||||
AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager = accessibilityManager,
|
||||
)
|
||||
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager =
|
||||
AccessibilityEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@@ -115,12 +110,6 @@ object AccessibilityModule {
|
||||
@ApplicationContext context: Context,
|
||||
): PackageManager = context.packageManager
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAccessibilityManager(
|
||||
@ApplicationContext context: Context,
|
||||
): AccessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesPowerManager(
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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.AppStateManager
|
||||
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,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
): AccessibilityActivityManager =
|
||||
AccessibilityActivityManagerImpl(
|
||||
context = context,
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
appStateManager = appStateManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
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.AppStateManager
|
||||
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,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) : AccessibilityActivityManager {
|
||||
init {
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
accessibilityEnabledManager.isAccessibilityEnabled =
|
||||
context.isAccessibilityServiceEnabled
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,15 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
*/
|
||||
interface AccessibilityEnabledManager {
|
||||
/**
|
||||
* Emits updates that track whether the accessibility autofill service is enabled..
|
||||
* 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>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -8,20 +7,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
/**
|
||||
* The default implementation of [AccessibilityEnabledManager].
|
||||
*/
|
||||
class AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
) : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
|
||||
value = accessibilityManager.isEnabled,
|
||||
)
|
||||
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
init {
|
||||
accessibilityManager.addAccessibilityStateChangeListener(
|
||||
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
|
||||
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
|
||||
},
|
||||
)
|
||||
}
|
||||
override var isAccessibilityEnabled: Boolean
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.value
|
||||
set(value) {
|
||||
mutableIsAccessibilityEnabledStateFlow.value = value
|
||||
}
|
||||
|
||||
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
|
||||
|
||||
@@ -8,6 +8,4 @@ import android.view.accessibility.AccessibilityNodeInfo
|
||||
data class FillableFields(
|
||||
val usernameField: AccessibilityNodeInfo?,
|
||||
val passwordFields: List<AccessibilityNodeInfo>,
|
||||
) {
|
||||
val hasFields: Boolean = usernameField != null || passwordFields.isNotEmpty()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,10 +35,12 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
if (!powerManager.isInteractive) return
|
||||
// We skip if the system package
|
||||
if (eventNode.isSystemPackage) return
|
||||
// We skip any package that is unsupported
|
||||
if (eventNode.shouldSkipPackage) return
|
||||
// We skip any package that is a launcher
|
||||
if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) {
|
||||
// We skip any package that is a launcher or unsupported
|
||||
if (eventNode.shouldSkipPackage ||
|
||||
launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }
|
||||
) {
|
||||
// Clear the action since this event needs to be ignored completely
|
||||
accessibilityAutofillManager.accessibilityAction = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,11 +65,6 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
|
||||
accessibilityParser
|
||||
.parseForUriOrPackageName(rootNode = rootNode)
|
||||
?.takeIf {
|
||||
accessibilityParser
|
||||
.parseForFillableFields(rootNode = rootNode, uri = it)
|
||||
.hasFields
|
||||
}
|
||||
?.let { uri ->
|
||||
context.startActivity(
|
||||
createAutofillSelectionIntent(
|
||||
|
||||
@@ -128,11 +128,6 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
|
||||
// 2nd = Anticipation
|
||||
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.ironfoxoss.ironfox",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
|
||||
// [DEPRECATED ENTRY]
|
||||
Browser(
|
||||
@@ -196,6 +191,11 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
|
||||
),
|
||||
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),
|
||||
Browser(
|
||||
packageName = "us.spotco.fennec_dos",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
|
||||
// [Section B] Entries only present here
|
||||
// TODO: Test the compatibility of these with Autofill Framework
|
||||
|
||||
@@ -8,9 +8,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -26,32 +23,19 @@ import dagger.hilt.android.scopes.ActivityScoped
|
||||
@InstallIn(ActivityComponent::class)
|
||||
object ActivityAutofillModule {
|
||||
|
||||
@ActivityScoped
|
||||
@ActivityScopedManager
|
||||
@Provides
|
||||
fun provideActivityScopedChromeThirdPartyAutofillManager(
|
||||
activity: Activity,
|
||||
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
|
||||
context = activity.baseContext,
|
||||
)
|
||||
|
||||
@ActivityScoped
|
||||
@Provides
|
||||
fun provideAutofillActivityManager(
|
||||
@ActivityScopedManager autofillManager: AutofillManager,
|
||||
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
appStateManager: AppStateManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
): AutofillActivityManager =
|
||||
AutofillActivityManagerImpl(
|
||||
autofillManager = autofillManager,
|
||||
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
|
||||
appStateManager = appStateManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,15 +15,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -57,15 +54,6 @@ object AutofillModule {
|
||||
fun providesAutofillEnabledManager(): AutofillEnabledManager =
|
||||
AutofillEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesChromeAutofillEnabledManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): ChromeThirdPartyAutofillEnabledManager =
|
||||
ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
|
||||
@@ -8,13 +8,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -46,8 +42,6 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
clock: Clock,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
@@ -58,34 +52,23 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
clock,
|
||||
biometricsEncryptionManager,
|
||||
featureFlagManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFido2CredentialManager(
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2OriginManager: Fido2OriginManager,
|
||||
json: Json,
|
||||
): Fido2CredentialManager =
|
||||
Fido2CredentialManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFido2OriginManager(
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
): Fido2OriginManager =
|
||||
Fido2OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
json = json,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
|
||||
@@ -24,6 +26,14 @@ interface Fido2CredentialManager {
|
||||
*/
|
||||
var authenticationAttempts: Int
|
||||
|
||||
/**
|
||||
* Attempt to validate the RP and origin of the provided [callingAppInfo] and [relyingPartyId].
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Attempt to extract FIDO 2 passkey attestation options from the system [requestJson], or null.
|
||||
*/
|
||||
@@ -43,7 +53,7 @@ interface Fido2CredentialManager {
|
||||
*/
|
||||
suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult
|
||||
|
||||
|
||||
@@ -6,37 +6,43 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class Fido2CredentialManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2OriginManager: Fido2OriginManager,
|
||||
private val json: Json,
|
||||
) : Fido2CredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@@ -47,49 +53,45 @@ class Fido2CredentialManagerImpl(
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
|
||||
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
|
||||
} else {
|
||||
callingAppInfo
|
||||
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
|
||||
)
|
||||
}
|
||||
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
Origin.Web(fido2CreateCredentialRequest.origin)
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val assetLinkUrl = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
val origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = fido2CredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
host = assetLinkUrl.toHostOrPathOrNull()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
assetLinkUrl = assetLinkUrl,
|
||||
),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
@@ -102,22 +104,20 @@ class Fido2CredentialManagerImpl(
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = {
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
|
||||
)
|
||||
},
|
||||
onFailure = { Fido2RegisterCredentialResult.Error },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validateOrigin(
|
||||
override suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
): Fido2ValidateOriginResult {
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPasskeyAttestationOptionsOrNull(
|
||||
requestJson: String,
|
||||
@@ -125,10 +125,8 @@ class Fido2CredentialManagerImpl(
|
||||
try {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
Timber.e(e, "Failed to decode passkey attestation options.")
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Failed to decode passkey attestation options.")
|
||||
null
|
||||
}
|
||||
|
||||
@@ -138,14 +136,11 @@ class Fido2CredentialManagerImpl(
|
||||
try {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
Timber.e(e, "Failed to decode passkey assertion options: $e")
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Failed to decode passkey assertion options: $e")
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
@@ -155,52 +150,39 @@ class Fido2CredentialManagerImpl(
|
||||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val origin = callingAppInfo.origin
|
||||
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
val relyingPartyId = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
|
||||
val validateOriginResult = validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
|
||||
Origin.Web(request.origin)
|
||||
} else {
|
||||
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
host = hostUrl,
|
||||
assetLinkUrl = hostUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
|
||||
Fido2CredentialAssertionResult.Error
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
callingAppInfo.packageName,
|
||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin.toHostOrPathOrNull()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin,
|
||||
),
|
||||
),
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
@@ -212,31 +194,146 @@ class Fido2CredentialManagerImpl(
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to authenticate FIDO2 credential.")
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_authentication_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
)
|
||||
},
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return digitalAssetLinkService
|
||||
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
.map { statements ->
|
||||
statements
|
||||
.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName = callingAppInfo.packageName,
|
||||
)
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
}
|
||||
.map { matchingStatements ->
|
||||
callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?.let { certificateFingerprint ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
signature = certificateFingerprint,
|
||||
)
|
||||
}
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotVerified
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
Fido2ValidateOriginResult.Success
|
||||
},
|
||||
onFailure = {
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult {
|
||||
val googleAllowListResult =
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
return when (googleAllowListResult) {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
// Application was found and successfully validated against the Google allow list so
|
||||
// we can return the result as the final validation result.
|
||||
googleAllowListResult
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
// Check the community allow list if the Google allow list failed, and return the
|
||||
// result as the final validation result.
|
||||
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
fileName: String,
|
||||
): Fido2ValidateOriginResult =
|
||||
assetManager
|
||||
.readAsset(fileName)
|
||||
.map { allowList ->
|
||||
callingAppInfo.validatePrivilegedApp(
|
||||
allowList = allowList,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns statements targeting the calling Android application, or null.
|
||||
*/
|
||||
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
filter { statement ->
|
||||
val target = statement.target
|
||||
target.namespace == "android_app" &&
|
||||
target.packageName == rpPackageName &&
|
||||
statement.relation.containsAll(
|
||||
listOf(
|
||||
"delegate_permission/common.get_login_creds",
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
),
|
||||
)
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
/**
|
||||
* Returns statements that match the given [signature], or null.
|
||||
*/
|
||||
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
|
||||
signature: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
filter { statement ->
|
||||
statement.target.sha256CertFingerprints
|
||||
?.contains(signature)
|
||||
?: false
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
?.prefixHttpsIfNecessaryOrNull()
|
||||
?.let { "$HTTPS$it" }
|
||||
|
||||
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAttestationOptionsOrNull(requestJson)
|
||||
?.relyingParty
|
||||
?.id
|
||||
?.prefixHttpsIfNecessaryOrNull()
|
||||
?.let { "$HTTPS$it" }
|
||||
}
|
||||
|
||||
private const val MAX_AUTHENTICATION_ATTEMPTS = 5
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Responsible for managing FIDO2 origin validation.
|
||||
*/
|
||||
interface Fido2OriginManager {
|
||||
|
||||
/**
|
||||
* Validates the origin of a calling app.
|
||||
*
|
||||
* @param callingAppInfo The calling app info.
|
||||
* @param relyingPartyId The relying party ID.
|
||||
*
|
||||
* @return The result of the validation.
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import timber.log.Timber
|
||||
|
||||
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2OriginManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class Fido2OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
) : Fido2OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult = digitalAssetLinkService
|
||||
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
.mapCatching { statements ->
|
||||
statements
|
||||
.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName = callingAppInfo.packageName,
|
||||
)
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
}
|
||||
.mapCatching { matchingStatements ->
|
||||
callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?.let { certificateFingerprint ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
signature = certificateFingerprint,
|
||||
)
|
||||
}
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
Fido2ValidateOriginResult.Success(null)
|
||||
},
|
||||
onFailure = {
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult {
|
||||
val googleAllowListResult =
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
return when (googleAllowListResult) {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
// Application was found and successfully validated against the Google allow list so
|
||||
// we can return the result as the final validation result.
|
||||
googleAllowListResult
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
// Check the community allow list if the Google allow list failed, and return the
|
||||
// result as the final validation result.
|
||||
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
fileName: String,
|
||||
): Fido2ValidateOriginResult =
|
||||
assetManager
|
||||
.readAsset(fileName)
|
||||
.mapCatching { allowList ->
|
||||
callingAppInfo.validatePrivilegedApp(
|
||||
allowList = allowList,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns statements targeting the calling Android application, or null.
|
||||
*/
|
||||
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
filter { statement ->
|
||||
val target = statement.target
|
||||
target.namespace == "android_app" &&
|
||||
target.packageName == rpPackageName &&
|
||||
statement.relation.containsAll(
|
||||
listOf(
|
||||
"delegate_permission/common.get_login_creds",
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
),
|
||||
)
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
/**
|
||||
* Returns statements that match the given [signature], or null.
|
||||
*/
|
||||
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
|
||||
signature: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
filter { statement ->
|
||||
statement.target.sha256CertFingerprints
|
||||
?.contains(signature)
|
||||
?: false
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
}
|
||||
@@ -7,19 +7,6 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 credential authentication request parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the Bitwarden user to authenticate.
|
||||
* @param cipherId The ID of the cipher that contains the passkey to authenticate.
|
||||
* @param credentialId The ID of the credential to authenticate.
|
||||
* @param requestJson The JSON representation of the FIDO 2 request.
|
||||
* @param clientDataHash The hash of the client data.
|
||||
* @param packageName The package name of the calling app.
|
||||
* @param signingInfo The signing info of the calling app.
|
||||
* @param origin The origin of the calling app. Only populated if the calling application is a
|
||||
* privileged application. I.e., a web browser.
|
||||
* @param isUserVerified Whether the user has been verified prior to receiving this request. Only
|
||||
* populated if device biometric verification was performed. If null, the application is responsible
|
||||
* for prompting user verification when it is deemed necessary.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
@@ -31,7 +18,6 @@ data class Fido2CredentialAssertionRequest(
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Represents possible outcomes of a FIDO 2 credential assertion request.
|
||||
*/
|
||||
@@ -15,5 +13,5 @@ sealed class Fido2CredentialAssertionResult {
|
||||
/**
|
||||
* Indicates there was an error and the assertion was not successful.
|
||||
*/
|
||||
data class Error(val message: Text) : Fido2CredentialAssertionResult()
|
||||
data object Error : Fido2CredentialAssertionResult()
|
||||
}
|
||||
|
||||
@@ -14,13 +14,12 @@ import kotlinx.parcelize.Parcelize
|
||||
* @property callingAppInfo Information about the application that initiated the request.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CreateCredentialRequest(
|
||||
data class Fido2CredentialRequest(
|
||||
val userId: String,
|
||||
val requestJson: String,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Represents the result of a FIDO 2 Get Credentials request.
|
||||
@@ -25,7 +24,5 @@ sealed class Fido2GetCredentialsResult {
|
||||
/**
|
||||
* Indicates an error was encountered when querying for matching credentials.
|
||||
*/
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : Fido2GetCredentialsResult()
|
||||
data object Error : Fido2GetCredentialsResult()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Models the data returned from creating a FIDO 2 credential.
|
||||
*/
|
||||
@@ -11,13 +9,13 @@ sealed class Fido2RegisterCredentialResult {
|
||||
* Indicates the credential has been successfully registered.
|
||||
*/
|
||||
data class Success(
|
||||
val responseJson: String,
|
||||
val registrationResponse: String,
|
||||
) : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates there was an error and the credential was not registered.
|
||||
*/
|
||||
data class Error(val message: Text) : Fido2RegisterCredentialResult()
|
||||
data object Error : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates the user cancelled the request.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Models the result of validating the origin of a FIDO2 request.
|
||||
*/
|
||||
@@ -10,75 +7,49 @@ sealed class Fido2ValidateOriginResult {
|
||||
|
||||
/**
|
||||
* Represents a successful origin validation.
|
||||
*
|
||||
* @param origin The origin of the calling app, or null if the calling app is not privileged.
|
||||
*/
|
||||
data class Success(val origin: String?) : Fido2ValidateOriginResult()
|
||||
data object Success : Fido2ValidateOriginResult()
|
||||
|
||||
/**
|
||||
* Represents a validation error.
|
||||
*/
|
||||
sealed class Error : Fido2ValidateOriginResult() {
|
||||
/**
|
||||
* The string resource ID of the error message.
|
||||
*/
|
||||
@get:StringRes
|
||||
abstract val messageResId: Int
|
||||
|
||||
/**
|
||||
* Indicates the digital asset links file could not be located.
|
||||
*/
|
||||
data object AssetLinkNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_of_missing_asset_links
|
||||
}
|
||||
data object AssetLinkNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates the application package name was not found in the digital asset links file.
|
||||
*/
|
||||
data object ApplicationNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
|
||||
}
|
||||
data object ApplicationNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates the application fingerprint was not found the digital asset links file.
|
||||
*/
|
||||
data object ApplicationFingerprintNotVerified : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
}
|
||||
data object ApplicationNotVerified : Error()
|
||||
|
||||
/**
|
||||
* Indicates the calling application is privileged but its package name is not found within
|
||||
* the privileged app allow list.
|
||||
*/
|
||||
data object PrivilegedAppNotAllowed : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_is_not_privileged
|
||||
}
|
||||
data object PrivilegedAppNotAllowed : Error()
|
||||
|
||||
/**
|
||||
* Indicates the calling app is privileged but but no matching signing certificate signature
|
||||
* is present in the allow list.
|
||||
*/
|
||||
data object PrivilegedAppSignatureNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_signature_does_not_match
|
||||
}
|
||||
data object PrivilegedAppSignatureNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates passkeys are not supported for the requesting application.
|
||||
*/
|
||||
data object PasskeyNotSupportedForApp : Error() {
|
||||
override val messageResId = R.string.passkeys_not_supported_for_this_app
|
||||
}
|
||||
data object PasskeyNotSupportedForApp : Error()
|
||||
|
||||
/**
|
||||
* Indicates an unknown error was encountered while validating the origin.
|
||||
*/
|
||||
data object Unknown : Error() {
|
||||
override val messageResId = R.string.generic_error_message
|
||||
}
|
||||
data object Unknown : Error()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>?,
|
||||
val transports: List<String>,
|
||||
)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.processor
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.OutcomeReceiver
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.credentials.exceptions.ClearCredentialException
|
||||
import androidx.credentials.exceptions.ClearCredentialUnsupportedException
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
@@ -24,35 +21,25 @@ import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.crypto.Cipher
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
|
||||
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
|
||||
@@ -62,7 +49,7 @@ const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOU
|
||||
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
|
||||
* processing.
|
||||
*/
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Suppress("LongParameterList")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class Fido2ProviderProcessorImpl(
|
||||
private val context: Context,
|
||||
@@ -72,8 +59,6 @@ class Fido2ProviderProcessorImpl(
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
private val clock: Clock,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
|
||||
@@ -104,6 +89,60 @@ class Fido2ProviderProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
|
||||
if (requestJson.isNullOrEmpty()) return null
|
||||
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
return CreateEntry
|
||||
.Builder(
|
||||
accountName = accountName,
|
||||
pendingIntent = intentManager.createFido2CreationPendingIntent(
|
||||
CREATE_PASSKEY_INTENT,
|
||||
userId,
|
||||
requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun processGetCredentialRequest(
|
||||
request: BeginGetCredentialRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
@@ -158,78 +197,6 @@ class Fido2ProviderProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
|
||||
if (requestJson.isNullOrEmpty()) return null
|
||||
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
val entryBuilder = CreateEntry
|
||||
.Builder(
|
||||
accountName = accountName,
|
||||
pendingIntent = intentManager.createFido2CreationPendingIntent(
|
||||
CREATE_PASSKEY_INTENT,
|
||||
userId,
|
||||
requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.setAutoSelectAllowed(true)
|
||||
|
||||
if (isVaultUnlocked &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation)
|
||||
) {
|
||||
biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId)
|
||||
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
|
||||
}
|
||||
return entryBuilder.build()
|
||||
}
|
||||
|
||||
@Throws(GetCredentialUnsupportedException::class)
|
||||
private suspend fun getMatchingFido2CredentialEntries(
|
||||
userId: String,
|
||||
@@ -256,14 +223,10 @@ class Fido2ProviderProcessorImpl(
|
||||
): List<CredentialEntry> {
|
||||
val cipherViews = vaultRepository
|
||||
.ciphersStateFlow
|
||||
.takeUntilLoaded()
|
||||
.fold(emptyList<CipherView>()) { _, dataState ->
|
||||
when (dataState) {
|
||||
is DataState.Loaded -> dataState.data.filter { it.isActiveWithFido2Credentials }
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
.value
|
||||
.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
?: emptyList()
|
||||
val result = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViews)
|
||||
return when (result) {
|
||||
@@ -289,70 +252,29 @@ class Fido2ProviderProcessorImpl(
|
||||
): List<CredentialEntry> =
|
||||
this
|
||||
.map {
|
||||
val publicKeyEntryBuilder = PublicKeyCredentialEntry
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = it.userNameForUi ?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(
|
||||
Icon.createWithResource(
|
||||
context,
|
||||
R.drawable.ic_bw_passkey,
|
||||
),
|
||||
)
|
||||
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)) {
|
||||
biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId)
|
||||
?.let {
|
||||
publicKeyEntryBuilder
|
||||
.setBiometricPromptDataIfSupported(cipher = it)
|
||||
}
|
||||
}
|
||||
publicKeyEntryBuilder.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): PublicKeyCredentialEntry.Builder {
|
||||
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = BiometricPromptData
|
||||
.Builder()
|
||||
.buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
}
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
}
|
||||
|
||||
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): CreateEntry.Builder {
|
||||
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = BiometricPromptData
|
||||
.Builder()
|
||||
.buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
private fun BiometricPromptData.Builder.buildPromptDataWithCipher(
|
||||
cipher: Cipher,
|
||||
): BiometricPromptData = BiometricPromptData.Builder()
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
|
||||
@@ -15,10 +15,10 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [Fido2CreateCredentialRequest] related to an ongoing FIDO 2
|
||||
* Checks if this [Intent] contains a [Fido2CredentialRequest] related to an ongoing FIDO 2
|
||||
* credential creation process.
|
||||
*/
|
||||
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
@@ -33,13 +33,12 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
|
||||
val userId = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2CreateCredentialRequest(
|
||||
return Fido2CredentialRequest(
|
||||
userId = userId,
|
||||
requestJson = createPublicKeyRequest.requestJson,
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = systemRequest.biometricPromptResult?.isSuccessful,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,8 +67,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
val isUserVerified = systemRequest.biometricPromptResult?.isSuccessful
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
@@ -79,7 +76,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -14,31 +11,19 @@ import kotlinx.coroutines.flow.onEach
|
||||
*/
|
||||
class AutofillActivityManagerImpl(
|
||||
private val autofillManager: AutofillManager,
|
||||
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
) : AutofillActivityManager {
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
get() = ChromeThirdPartyAutofillStatus(
|
||||
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
)
|
||||
|
||||
init {
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
|
||||
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
|
||||
chromeAutofillStatus
|
||||
}
|
||||
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@@ -35,10 +34,7 @@ class AutofillTotpManagerImpl(
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(
|
||||
text = totpResult.code,
|
||||
toastDescriptorOverride = R.string.verification_code_totp.asText(),
|
||||
)
|
||||
clipboardManager.setText(text = totpResult.code)
|
||||
Toast
|
||||
.makeText(
|
||||
context.applicationContext,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager which provides whether specific Chrome versions have third party autofill available and
|
||||
* enabled.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillEnabledManager {
|
||||
/**
|
||||
* Combined status for all concerned Chrome versions.
|
||||
*/
|
||||
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
|
||||
/**
|
||||
* An observable [StateFlow] of the combined third party autofill status of all concerned
|
||||
* chrome versions.
|
||||
*/
|
||||
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : ChromeThirdPartyAutofillEnabledManager {
|
||||
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
|
||||
set(value) {
|
||||
field = value
|
||||
mutableChromeThirdPartyAutofillStatusStateFlow.update {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
|
||||
chromeThirdPartyAutofillStatus,
|
||||
)
|
||||
|
||||
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
get() = mutableChromeThirdPartyAutofillStatusStateFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
|
||||
) { data, enabled ->
|
||||
if (enabled) {
|
||||
data
|
||||
} else {
|
||||
DEFAULT_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* Manager class used to determine if a device has installed versions of Chrome (either the
|
||||
* stable release or beta channel) which support and require opt in to third party autofill.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillManager {
|
||||
|
||||
/**
|
||||
* The data representing the status of the stable chrome version
|
||||
*/
|
||||
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* The data representing the status of the beta chrome version
|
||||
*/
|
||||
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
|
||||
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
|
||||
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
|
||||
|
||||
/**
|
||||
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
|
||||
* [ContentResolver] to determine if the installed Chrome packages support and enable
|
||||
* third party autofill services.
|
||||
*
|
||||
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ChromeThirdPartyAutofillManagerImpl(
|
||||
private val context: Context,
|
||||
) : ChromeThirdPartyAutofillManager {
|
||||
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
|
||||
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
|
||||
|
||||
private fun getThirdPartyAutoFillStatusForChannel(
|
||||
releaseChannel: ChromeReleaseChannel,
|
||||
): ChromeThirdPartyAutoFillData {
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
|
||||
.path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
|
||||
.build()
|
||||
val cursor = context
|
||||
.contentResolver
|
||||
.query(
|
||||
/* uri = */ uri,
|
||||
/* projection = */ arrayOf(THIRD_PARTY_MODE_COLUMN),
|
||||
/* selection = */ null,
|
||||
/* selectionArgs = */ null,
|
||||
/* sortOrder = */ null,
|
||||
)
|
||||
var thirdPartyEnabled = false
|
||||
val isThirdPartyAvailable = cursor
|
||||
?.let {
|
||||
it.moveToFirst()
|
||||
val columnIndex = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
|
||||
thirdPartyEnabled = it.getInt(columnIndex) != 0
|
||||
it.close()
|
||||
true
|
||||
}
|
||||
?: false
|
||||
return ChromeThirdPartyAutoFillData(
|
||||
isAvailable = isThirdPartyAvailable,
|
||||
isThirdPartyEnabled = thirdPartyEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
|
||||
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
|
||||
|
||||
/**
|
||||
* Enumerated values of each version of Chrome supported for third party autofill checks.
|
||||
*
|
||||
* @property packageName the package name of the release channel for the Chrome version.
|
||||
*/
|
||||
enum class ChromeReleaseChannel(val packageName: String) {
|
||||
STABLE(CHROME_CHANNEL_PACKAGE),
|
||||
BETA(BETA_CHANNEL_PACKAGE),
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
/**
|
||||
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
|
||||
*/
|
||||
data class ChromeThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant release channels of Chrome.
|
||||
*/
|
||||
data class ChromeThirdPartyAutofillStatus(
|
||||
val stableStatusData: ChromeThirdPartyAutoFillData,
|
||||
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
|
||||
)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
@@ -148,12 +147,3 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
|
||||
getBundleExtra(AUTOFILL_BUNDLE_KEY)
|
||||
?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the
|
||||
* vault if one of the Autofill services starts the only instance of the [MainActivity].
|
||||
*/
|
||||
val Activity.createdForAutofill: Boolean
|
||||
get() = intent.getAutofillSelectionDataOrNull() != null ||
|
||||
intent.getAutofillSaveItemOrNull() != null ||
|
||||
intent.getAutofillAssistStructureOrNull() != null
|
||||
|
||||
@@ -24,6 +24,7 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
.uri
|
||||
?.replace("https://", "")
|
||||
?.replace("http://", "")
|
||||
?.replace("androidapp://", "")
|
||||
|
||||
AutofillSaveItem.Login(
|
||||
username = partition.usernameSaveValue,
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val SERVER_CONFIGURATIONS = "serverConfigurations"
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEv
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
@@ -19,11 +18,6 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
var appLanguage: AppLanguage?
|
||||
|
||||
/**
|
||||
* Emits updates that track [AppLanguage].
|
||||
*/
|
||||
val appLanguageFlow: Flow<AppLanguage?>
|
||||
|
||||
/**
|
||||
* Has the initial autofill dialog been shown to the user.
|
||||
*/
|
||||
@@ -74,6 +68,12 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* The instant when the last database scheme change was applied. `null` if no scheme changes
|
||||
* have been applied yet.
|
||||
*/
|
||||
var lastDatabaseSchemeChangeInstant: Instant?
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
@@ -314,92 +314,4 @@ interface SettingsDiskSource {
|
||||
* Emits updates that track [getShowImportLoginsSettingBadge] for the given [userId].
|
||||
*/
|
||||
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has registered for export via the credential exchange
|
||||
* protocol.
|
||||
*/
|
||||
fun getVaultRegisteredForExport(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the given [userId] has registered for export via
|
||||
* the credential exchange protocol.
|
||||
*/
|
||||
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
|
||||
*/
|
||||
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the number of qualifying add cipher actions for the device.
|
||||
*/
|
||||
fun getAddCipherActionCount(): Int?
|
||||
|
||||
/**
|
||||
* Stores the given [count] completed "add" cipher actions taken place on the device.
|
||||
*/
|
||||
fun storeAddCipherActionCount(count: Int?)
|
||||
|
||||
/**
|
||||
* Gets the number of qualifying generated result actions for the device.
|
||||
*/
|
||||
fun getGeneratedResultActionCount(): Int?
|
||||
|
||||
/**
|
||||
* Stores the given [count] completed generated password or username result actions taken
|
||||
* for the device.
|
||||
*/
|
||||
fun storeGeneratedResultActionCount(count: Int?)
|
||||
|
||||
/**
|
||||
* Gets the number of qualifying create send actions for the device.
|
||||
*/
|
||||
fun getCreateSendActionCount(): Int?
|
||||
|
||||
/**
|
||||
* Stores the given [count] completed create send actions for the device.
|
||||
*/
|
||||
fun storeCreateSendActionCount(count: Int?)
|
||||
|
||||
/**
|
||||
* Gets the Boolean value of if the Add Login CoachMark tour has been interacted with.
|
||||
*/
|
||||
fun getShouldShowAddLoginCoachMark(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores a value for if the Add Login CoachMark tour has been interacted with
|
||||
*/
|
||||
fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?)
|
||||
|
||||
/**
|
||||
* Returns an [Flow] to observe updates to the "ShouldShowAddLoginCoachMark" value.
|
||||
*/
|
||||
fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the Boolean value of if the Generator CoachMark tour has been interacted with.
|
||||
*/
|
||||
fun getShouldShowGeneratorCoachMark(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores a value for if the Generator CoachMark tour has been interacted with
|
||||
*/
|
||||
fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?)
|
||||
|
||||
/**
|
||||
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
|
||||
*/
|
||||
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the given [screenData] as the screen to resume to identified by [userId].
|
||||
*/
|
||||
fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?)
|
||||
|
||||
/**
|
||||
* Gets the screen data to resume to for the device identified by [userId] or null if no screen
|
||||
*/
|
||||
fun getAppResumeScreen(userId: String): AppResumeScreenData?
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@@ -11,6 +10,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
|
||||
@@ -36,13 +36,7 @@ private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedI
|
||||
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
|
||||
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
|
||||
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
|
||||
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
|
||||
private const val ADD_ACTION_COUNT = "addActionCount"
|
||||
private const val COPY_ACTION_COUNT = "copyActionCount"
|
||||
private const val CREATE_ACTION_COUNT = "createActionCount"
|
||||
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
|
||||
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
|
||||
private const val RESUME_SCREEN = "resumeScreen"
|
||||
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -53,7 +47,6 @@ class SettingsDiskSourceImpl(
|
||||
private val json: Json,
|
||||
) : BaseDiskSource(sharedPreferences = sharedPreferences),
|
||||
SettingsDiskSource {
|
||||
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
|
||||
private val mutableAppThemeFlow = bufferedMutableSharedFlow<AppTheme>(replay = 1)
|
||||
|
||||
private val mutableLastSyncFlowMap = mutableMapOf<String, MutableSharedFlow<Instant?>>()
|
||||
@@ -82,16 +75,9 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableVaultRegisteredForExportFlow =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = getString(key = APP_LANGUAGE_KEY)
|
||||
?.let { storedValue ->
|
||||
@@ -102,12 +88,8 @@ class SettingsDiskSourceImpl(
|
||||
key = APP_LANGUAGE_KEY,
|
||||
value = value?.localeName,
|
||||
)
|
||||
mutableAppLanguageFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val appLanguageFlow: Flow<AppLanguage?>
|
||||
get() = mutableAppLanguageFlow.onSubscription { emit(appLanguage) }
|
||||
|
||||
override var initialAutofillDialogShown: Boolean?
|
||||
get() = getBoolean(key = INITIAL_AUTOFILL_DIALOG_SHOWN)
|
||||
set(value) {
|
||||
@@ -174,6 +156,10 @@ class SettingsDiskSourceImpl(
|
||||
get() = mutableHasUserLoggedInOrCreatedAccountFlow
|
||||
.onSubscription { emit(getBoolean(HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY)) }
|
||||
|
||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
|
||||
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
@@ -186,16 +172,12 @@ class SettingsDiskSourceImpl(
|
||||
storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
|
||||
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
|
||||
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
|
||||
storeAppResumeScreen(userId = userId, screenData = null)
|
||||
|
||||
// The following are intentionally not cleared so they can be
|
||||
// restored after logging out and back in:
|
||||
// - screen capture allowed
|
||||
// - show autofill setting badge
|
||||
// - show unlock setting badge
|
||||
// - should show add login coach mark
|
||||
// - should show generator coach mark
|
||||
}
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
@@ -452,93 +434,6 @@ class SettingsDiskSourceImpl(
|
||||
getMutableShowImportLoginsSettingBadgeFlow(userId)
|
||||
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
|
||||
|
||||
override fun getVaultRegisteredForExport(userId: String): Boolean? =
|
||||
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId))
|
||||
|
||||
override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?) {
|
||||
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId), isRegistered)
|
||||
getMutableVaultRegisteredForExportFlow(userId).tryEmit(isRegistered)
|
||||
}
|
||||
|
||||
override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableVaultRegisteredForExportFlow(userId)
|
||||
.onSubscription { emit(getVaultRegisteredForExport(userId)) }
|
||||
|
||||
override fun getAddCipherActionCount(): Int? = getInt(
|
||||
key = ADD_ACTION_COUNT,
|
||||
)
|
||||
|
||||
override fun storeAddCipherActionCount(count: Int?) {
|
||||
putInt(
|
||||
key = ADD_ACTION_COUNT,
|
||||
value = count,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGeneratedResultActionCount(): Int? = getInt(
|
||||
key = COPY_ACTION_COUNT,
|
||||
)
|
||||
|
||||
override fun storeGeneratedResultActionCount(count: Int?) {
|
||||
putInt(
|
||||
key = COPY_ACTION_COUNT,
|
||||
value = count,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCreateSendActionCount(): Int? = getInt(
|
||||
key = CREATE_ACTION_COUNT,
|
||||
)
|
||||
|
||||
override fun storeCreateSendActionCount(count: Int?) {
|
||||
putInt(
|
||||
key = CREATE_ACTION_COUNT,
|
||||
value = count,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShouldShowAddLoginCoachMark(): Boolean? =
|
||||
getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK)
|
||||
|
||||
override fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK,
|
||||
value = shouldShow,
|
||||
)
|
||||
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(shouldShow)
|
||||
}
|
||||
|
||||
override fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
|
||||
emit(getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK))
|
||||
}
|
||||
|
||||
override fun getShouldShowGeneratorCoachMark(): Boolean? =
|
||||
getBoolean(key = SHOULD_SHOW_GENERATOR_COACH_MARK)
|
||||
|
||||
override fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOULD_SHOW_GENERATOR_COACH_MARK,
|
||||
value = shouldShow,
|
||||
)
|
||||
mutableHasSeenGeneratorCoachMarkFlow.tryEmit(shouldShow)
|
||||
}
|
||||
|
||||
override fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenGeneratorCoachMarkFlow.onSubscription {
|
||||
emit(getShouldShowGeneratorCoachMark())
|
||||
}
|
||||
|
||||
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
|
||||
putString(
|
||||
key = RESUME_SCREEN.appendIdentifier(userId),
|
||||
value = screenData?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAppResumeScreen(userId: String): AppResumeScreenData? =
|
||||
getString(RESUME_SCREEN.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
@@ -589,10 +484,4 @@ class SettingsDiskSourceImpl(
|
||||
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableVaultRegisteredForExportFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableVaultRegisteredForExportFlow.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,7 @@ object PlatformDiskModule {
|
||||
fun provideEventDatabase(
|
||||
app: Application,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
): PlatformDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
@@ -82,7 +84,12 @@ object PlatformDiskModule {
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addTypeConverter(ZonedDateTimeTypeConverter())
|
||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||
.addCallback(
|
||||
DatabaseSchemeCallback(
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -172,7 +172,7 @@ private class AndroidKeyStore(
|
||||
private val useSymmetricPreferenceKey: String = "essentials_use_symmetric"
|
||||
|
||||
private val prefsMasterKey = "SecureStorageKey"
|
||||
private val initializationVectorLen = 12 // Android supports an IV of 12 for AES/GCM
|
||||
private val initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM
|
||||
|
||||
init {
|
||||
keyStore.load(null)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Represents a mutual TLS certificate.
|
||||
*/
|
||||
data class MutualTlsCertificate(
|
||||
val alias: String,
|
||||
val privateKey: PrivateKey,
|
||||
val certificateChain: List<X509Certificate>,
|
||||
) {
|
||||
/**
|
||||
* Leaf certificate of the chain.
|
||||
*/
|
||||
val leafCertificate: X509Certificate?
|
||||
get() = certificateChain.lastOrNull()
|
||||
|
||||
/**
|
||||
* Root certificate of the chain.
|
||||
*/
|
||||
val rootCertificate: X509Certificate?
|
||||
get() = certificateChain.firstOrNull()
|
||||
|
||||
override fun toString(): String = leafCertificate
|
||||
?.let {
|
||||
buildString {
|
||||
appendLine("Subject: ${it.subjectDN}")
|
||||
appendLine("Issuer: ${it.issuerDN}")
|
||||
appendLine("Valid From: ${it.notBefore}")
|
||||
appendLine("Valid Until: ${it.notAfter}")
|
||||
}
|
||||
}
|
||||
?: ""
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
/**
|
||||
* Location of the key data.
|
||||
*/
|
||||
enum class MutualTlsKeyHost {
|
||||
/**
|
||||
* Key is stored in the system key chain.
|
||||
*/
|
||||
KEY_CHAIN,
|
||||
|
||||
/**
|
||||
* Key is stored in a private instance of the Android Key Store.
|
||||
*/
|
||||
ANDROID_KEY_STORE,
|
||||
}
|
||||
@@ -14,10 +14,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -74,17 +70,6 @@ object PlatformNetworkModule {
|
||||
@Singleton
|
||||
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSslManager(
|
||||
keyManager: KeyManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
): SslManager =
|
||||
SslManagerImpl(
|
||||
keyManager = keyManager,
|
||||
environmentRepository = environmentRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofits(
|
||||
@@ -92,7 +77,6 @@ object PlatformNetworkModule {
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
sslManager: SslManager,
|
||||
json: Json,
|
||||
): Retrofits =
|
||||
RetrofitsImpl(
|
||||
@@ -100,7 +84,6 @@ object PlatformNetworkModule {
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptor,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
sslManager = sslManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user