mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 19:36:34 -05:00
Compare commits
1 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54456d3d4f |
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"
|
||||
36
.github/workflows/build.yml
vendored
36
.github/workflows/build.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
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@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -253,7 +253,7 @@ 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
|
||||
- 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
# 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
|
||||
- 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -332,7 +332,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -340,7 +340,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -348,7 +348,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -356,7 +356,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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -515,7 +515,7 @@ 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -527,14 +527,14 @@ jobs:
|
||||
> ./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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
@@ -546,7 +546,7 @@ jobs:
|
||||
> ./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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
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"
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
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"
|
||||
2
.github/workflows/crowdin-push.yml
vendored
2
.github/workflows/crowdin-push.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
|
||||
with:
|
||||
tag_name: "v${{ inputs.version-name }}"
|
||||
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
|
||||
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"
|
||||
3
.github/workflows/scan-ci.yml
vendored
3
.github/workflows/scan-ci.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -58,4 +58,3 @@ jobs:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
|
||||
5
.github/workflows/scan.yml
vendored
5
.github/workflows/scan.yml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
@@ -41,7 +43,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@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -68,4 +70,3 @@ jobs:
|
||||
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"
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
bundle exec fastlane check
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports
|
||||
@@ -90,19 +90,17 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
if: success()
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
name: test-reports
|
||||
|
||||
- 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
|
||||
with:
|
||||
os: linux
|
||||
@@ -110,7 +108,7 @@ jobs:
|
||||
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')
|
||||
if: steps.upload-to-codecov.outcome == 'failure'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
24
Gemfile.lock
24
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.1027.0)
|
||||
aws-sdk-core (3.214.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.96.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.176.1)
|
||||
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)
|
||||
@@ -68,7 +68,7 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.226.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
@@ -111,7 +111,7 @@ GEM
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.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)
|
||||
@@ -163,7 +163,7 @@ GEM
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
jwt (2.10.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)
|
||||
@@ -185,7 +185,7 @@ GEM
|
||||
rexml (3.4.0)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
|
||||
@@ -325,7 +325,6 @@ kover {
|
||||
"*_*Factory\$*",
|
||||
"*.Hilt_*",
|
||||
"*_HiltModules",
|
||||
"*_HiltModules*",
|
||||
"*_HiltModules\$*",
|
||||
"*_Impl",
|
||||
"*_Impl\$*",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,20 +12,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"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,18 +36,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": {
|
||||
@@ -85,6 +59,34 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,7 +11,6 @@ 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
|
||||
@@ -20,7 +19,6 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
|
||||
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
|
||||
@@ -55,7 +53,6 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
@@ -69,14 +66,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 +94,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,
|
||||
fido2CreateCredentialRequest = 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()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJso
|
||||
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.
|
||||
@@ -353,14 +352,4 @@ interface AuthDiskSource {
|
||||
* 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?)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ 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
|
||||
@@ -50,7 +50,6 @@ 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].
|
||||
@@ -156,7 +155,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.
|
||||
@@ -505,19 +503,6 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,6 @@ interface IdentityService {
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
newDeviceOtp: String? = null,
|
||||
): Result<GetTokenResponseJson>
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,6 @@ class IdentityServiceImpl(
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
newDeviceOtp: String?,
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
@@ -80,28 +79,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 +102,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,7 +131,6 @@ class IdentityServiceImpl(
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson> {
|
||||
@@ -166,7 +149,6 @@ class IdentityServiceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
|
||||
@@ -230,19 +230,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 +252,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 +362,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,7 +401,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
/**
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(status: OnboardingStatus)
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
|
||||
/**
|
||||
* Checks if a new device notice should be displayed.
|
||||
|
||||
@@ -17,13 +17,11 @@ 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
|
||||
@@ -226,11 +224,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
|
||||
|
||||
/**
|
||||
@@ -690,26 +683,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 +764,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 +1088,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
@@ -1200,21 +1162,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 +1195,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(
|
||||
@@ -1384,13 +1333,8 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setOnboardingStatus(status: OnboardingStatus) {
|
||||
activeUserId?.let { userId ->
|
||||
authDiskSource.storeOnboardingStatus(
|
||||
userId = userId,
|
||||
onboardingStatus = status,
|
||||
)
|
||||
}
|
||||
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
|
||||
@@ -1428,7 +1372,6 @@ class AuthRepositoryImpl(
|
||||
// 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
|
||||
@@ -1634,7 +1577,6 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
uniqueAppId = authDiskSource.uniqueAppId,
|
||||
@@ -1642,7 +1584,6 @@ class AuthRepositoryImpl(
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { throwable ->
|
||||
@@ -1651,7 +1592,6 @@ class AuthRepositoryImpl(
|
||||
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
|
||||
LoginResult.UnofficialServerError
|
||||
}
|
||||
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
@@ -1676,22 +1616,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 +1706,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 +1743,6 @@ class AuthRepositoryImpl(
|
||||
twoFactorResponse = null
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
resendNewDeviceOtpRequestJson = null
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
@@ -1840,24 +1775,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.
|
||||
*/
|
||||
|
||||
@@ -33,9 +33,4 @@ sealed class 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)
|
||||
|
||||
@@ -18,4 +18,5 @@ data class Organization(
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
val shouldUsersGetPremium: Boolean,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
shouldUsersGetPremium = this.shouldUsersGetPremium,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
class AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
) : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
|
||||
value = accessibilityManager.isEnabled,
|
||||
)
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
init {
|
||||
accessibilityManager.addAccessibilityStateChangeListener(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -13,8 +13,6 @@ 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 +44,6 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
clock: Clock,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
@@ -58,8 +54,6 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
clock,
|
||||
biometricsEncryptionManager,
|
||||
featureFlagManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
@@ -23,11 +22,10 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
|
||||
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
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialManager].
|
||||
@@ -50,45 +48,41 @@ class Fido2CredentialManagerImpl(
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
|
||||
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
|
||||
} else {
|
||||
callingAppInfo
|
||||
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CreateCredentialRequest
|
||||
.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 = fido2CreateCredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val assetLinkUrl = fido2CreateCredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
val origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = fido2CreateCredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CreateCredentialRequest
|
||||
.callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
host = assetLinkUrl.toHostOrPathOrNull()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
assetLinkUrl = assetLinkUrl,
|
||||
),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
@@ -102,11 +96,7 @@ 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 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,10 +115,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 +126,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,44 +140,22 @@ 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 -> {
|
||||
@@ -200,7 +163,16 @@ class Fido2CredentialManagerImpl(
|
||||
.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,13 +184,7 @@ 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -230,13 +196,13 @@ class Fido2CredentialManagerImpl(
|
||||
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
|
||||
|
||||
@@ -20,4 +20,13 @@ interface Fido2OriginManager {
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Returns the privileged app origin, or null if the calling app is not allowed.
|
||||
*
|
||||
* @param callingAppInfo The calling app info.
|
||||
*
|
||||
* @return The privileged app origin, or null.
|
||||
*/
|
||||
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ class Fido2OriginManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
|
||||
if (!callingAppInfo.isOriginPopulated()) return null
|
||||
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
|
||||
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
|
||||
?.takeUnless { !callingAppInfo.isOriginPopulated() }
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
@@ -116,10 +123,7 @@ class Fido2OriginManagerImpl(
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -153,4 +157,16 @@ class Fido2OriginManagerImpl(
|
||||
?: false
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
private suspend fun getGoogleAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
|
||||
.getOrNull()
|
||||
|
||||
private suspend fun getCommunityAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ data class Fido2CreateCredentialRequest(
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>?,
|
||||
val transports: List<String>,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ 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,7 +22,6 @@ 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
|
||||
@@ -37,13 +34,9 @@ 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
|
||||
@@ -52,7 +45,6 @@ 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 +54,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 +64,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 +94,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 +202,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,
|
||||
@@ -289,70 +261,36 @@ 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,
|
||||
),
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ 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
|
||||
* credential creation process.
|
||||
*/
|
||||
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
fun Intent.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
@@ -39,7 +39,6 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
|
||||
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,7 @@ 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 com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@@ -25,8 +25,15 @@ class AutofillTotpManagerImpl(
|
||||
) : AutofillTotpManager {
|
||||
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val organizationPremiumStatusMap = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.getOrganizationPremiumStatusMap()
|
||||
.orEmpty()
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
val premiumStatus = organizationPremiumStatusMap[cipherView.organizationId] ?: isPremium
|
||||
if (!premiumStatus && !cipherView.organizationUseTotp) return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
@@ -35,10 +42,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,
|
||||
)
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -362,44 +356,4 @@ interface SettingsDiskSource {
|
||||
* 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
|
||||
|
||||
@@ -40,9 +40,6 @@ 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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -53,7 +50,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,10 +78,6 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -102,12 +94,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) {
|
||||
@@ -187,15 +175,12 @@ class SettingsDiskSourceImpl(
|
||||
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(
|
||||
@@ -497,48 +482,6 @@ class SettingsDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
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?> =
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -15,9 +14,6 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
@@ -28,7 +24,6 @@ class RetrofitsImpl(
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
json: Json,
|
||||
private val sslManager: SslManager,
|
||||
) : Retrofits {
|
||||
//region Authenticated Retrofits
|
||||
|
||||
@@ -72,10 +67,6 @@ class RetrofitsImpl(
|
||||
baseClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
@@ -102,10 +93,6 @@ class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.authenticator(refreshAuthenticator)
|
||||
.addInterceptor(authTokenInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -146,22 +133,9 @@ class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun OkHttpClient.Builder.setSslSocketFactory(
|
||||
sslContext: SSLContext,
|
||||
trustManagers: Array<TrustManager>,
|
||||
): OkHttpClient.Builder =
|
||||
sslSocketFactory(
|
||||
sslContext.socketFactory,
|
||||
trustManagers.first() as X509TrustManager,
|
||||
)
|
||||
|
||||
//endregion Helper properties and functions
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import kotlinx.serialization.encoding.Encoder
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
private val className: String,
|
||||
private val values: Array<T>,
|
||||
private val default: T? = null,
|
||||
) : KSerializer<T> {
|
||||
@@ -30,7 +29,7 @@ abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
val decodedValue = decoder.decodeInt().toString()
|
||||
return values.firstOrNull { it.serialNameAnnotation?.value == decodedValue }
|
||||
?: default
|
||||
?: throw IllegalArgumentException("Unknown value $decodedValue for $className")
|
||||
?: throw IllegalArgumentException("Unknown value $decodedValue")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
/**
|
||||
* Interface for managing SSL connections.
|
||||
*/
|
||||
interface SslManager {
|
||||
|
||||
/**
|
||||
* The SSL context to use for SSL connections.
|
||||
*/
|
||||
val sslContext: SSLContext
|
||||
|
||||
/**
|
||||
* The trust managers to use for SSL connections.
|
||||
*/
|
||||
val trustManagers: Array<TrustManager>
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedKeyManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [SslManager].
|
||||
*/
|
||||
class SslManagerImpl(
|
||||
private val keyManager: KeyManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
) : SslManager {
|
||||
|
||||
/*
|
||||
This property must only be accessed from a background thread. Accessing this property from
|
||||
the main thread will result in an exception being thrown when retrieving the mutual TLS
|
||||
certificate from [KeyManager].
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
@get:WorkerThread
|
||||
internal val mutualTlsCertificate: MutualTlsCertificate?
|
||||
get() {
|
||||
val keyUri = getKeyUri()
|
||||
?: return null
|
||||
|
||||
val host = MutualTlsKeyHost
|
||||
.entries
|
||||
.find { it.name == keyUri.authority }
|
||||
?: return null
|
||||
|
||||
val alias = keyUri.path
|
||||
?.trim('/')
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return null
|
||||
|
||||
return keyManager.getMutualTlsCertificateChain(
|
||||
alias = alias,
|
||||
host = host,
|
||||
)
|
||||
}
|
||||
|
||||
override val trustManagers: Array<TrustManager>
|
||||
get() = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
.apply { init(null as KeyStore?) }
|
||||
.trustManagers
|
||||
|
||||
override val sslContext: SSLContext
|
||||
get() = SSLContext
|
||||
.getInstance("TLS")
|
||||
.apply {
|
||||
init(
|
||||
arrayOf(X509ExtendedKeyManagerImpl()),
|
||||
trustManagers,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getKeyUri(): Uri? = environmentRepository
|
||||
.environment
|
||||
.environmentUrlData
|
||||
.keyUri
|
||||
?.toUri()
|
||||
|
||||
private inner class X509ExtendedKeyManagerImpl : X509ExtendedKeyManager() {
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<out String>?,
|
||||
issuers: Array<out Principal>?,
|
||||
socket: Socket?,
|
||||
): String = mutualTlsCertificate?.alias ?: ""
|
||||
|
||||
override fun getCertificateChain(
|
||||
alias: String?,
|
||||
): Array<X509Certificate>? =
|
||||
mutualTlsCertificate
|
||||
?.certificateChain
|
||||
?.toTypedArray()
|
||||
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? =
|
||||
mutualTlsCertificate
|
||||
?.privateKey
|
||||
|
||||
//region Unused server side methods
|
||||
override fun getServerAliases(
|
||||
alias: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
): Array<String> = arrayOf()
|
||||
|
||||
override fun getClientAliases(
|
||||
keyType: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
): Array<String> = emptyArray()
|
||||
|
||||
override fun chooseServerAlias(
|
||||
alias: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
socket: Socket?,
|
||||
): String = ""
|
||||
//endregion Unused server side methods
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
|
||||
/**
|
||||
* Manages the screen from which the app should be resumed after unlock.
|
||||
*/
|
||||
interface AppResumeManager {
|
||||
|
||||
/**
|
||||
* Sets the screen from which the app should be resumed after unlock.
|
||||
*
|
||||
* @param screenData The screen identifier (e.g., "HomeScreen", "SettingsScreen").
|
||||
*/
|
||||
fun setResumeScreen(screenData: AppResumeScreenData)
|
||||
|
||||
/**
|
||||
* Gets the screen from which the app should be resumed after unlock.
|
||||
*
|
||||
* @return The screen identifier, or an empty string if not set.
|
||||
*/
|
||||
fun getResumeScreen(): AppResumeScreenData?
|
||||
|
||||
/**
|
||||
* Gets the special circumstance associated with the resume screen for the current user.
|
||||
*
|
||||
* @return The special circumstance, or null if no special circumstance
|
||||
* is associated with the resume screen.
|
||||
*/
|
||||
fun getResumeSpecialCircumstance(): SpecialCircumstance?
|
||||
|
||||
/**
|
||||
* Clears the saved resume screen for the current user.
|
||||
*/
|
||||
fun clearResumeScreen()
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import java.time.Clock
|
||||
|
||||
private const val UNLOCK_NAVIGATION_TIME_SECONDS: Long = 5 * 60
|
||||
|
||||
/**
|
||||
* Primary implementation of [AppResumeManager].
|
||||
*/
|
||||
class AppResumeManagerImpl(
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val clock: Clock,
|
||||
) : AppResumeManager {
|
||||
|
||||
override fun setResumeScreen(screenData: AppResumeScreenData) {
|
||||
authRepository.activeUserId?.let {
|
||||
settingsDiskSource.storeAppResumeScreen(
|
||||
userId = it,
|
||||
screenData = screenData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResumeScreen(): AppResumeScreenData? {
|
||||
return authRepository.activeUserId?.let { userId ->
|
||||
settingsDiskSource.getAppResumeScreen(userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResumeSpecialCircumstance(): SpecialCircumstance? {
|
||||
val userId = authRepository.activeUserId ?: return null
|
||||
val timeNowMinus5Min = clock.instant().minusSeconds(UNLOCK_NAVIGATION_TIME_SECONDS)
|
||||
val lastLockTimestamp = authDiskSource
|
||||
.getLastLockTimestamp(userId = userId)
|
||||
?: return null
|
||||
|
||||
if (timeNowMinus5Min.isAfter(lastLockTimestamp)) {
|
||||
settingsDiskSource.storeAppResumeScreen(userId = userId, screenData = null)
|
||||
return null
|
||||
}
|
||||
return when (val resumeScreenData = getResumeScreen()) {
|
||||
AppResumeScreenData.GeneratorScreen -> SpecialCircumstance.GeneratorShortcut
|
||||
AppResumeScreenData.SendScreen -> SpecialCircumstance.SendShortcut
|
||||
is AppResumeScreenData.SearchScreen -> SpecialCircumstance.SearchShortcut(
|
||||
searchTerm = resumeScreenData.searchTerm,
|
||||
)
|
||||
|
||||
AppResumeScreenData.VerificationCodeScreen -> {
|
||||
SpecialCircumstance.VerificationCodeShortcut
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearResumeScreen() {
|
||||
val userId = authRepository.activeUserId ?: return
|
||||
if (vaultLockManager.isVaultUnlocked(userId = userId)) {
|
||||
settingsDiskSource.storeAppResumeScreen(
|
||||
userId = userId,
|
||||
screenData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,6 @@ interface BiometricsEncryptionManager {
|
||||
userId: String,
|
||||
): Cipher?
|
||||
|
||||
/**
|
||||
* Clears the data associated with the users biometrics.
|
||||
*/
|
||||
fun clearBiometrics(userId: String)
|
||||
|
||||
/**
|
||||
* Gets the [Cipher] built from a keystore, or creates one if it doesn't already exist.
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,6 @@ import javax.crypto.spec.IvParameterSpec
|
||||
* Default implementation of [BiometricsEncryptionManager] for managing Android keystore encryption
|
||||
* and decryption.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@OmitFromCoverage
|
||||
class BiometricsEncryptionManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
@@ -36,8 +35,20 @@ class BiometricsEncryptionManagerImpl(
|
||||
.getInstance(ENCRYPTION_KEYSTORE_NAME)
|
||||
.also { it.load(null) }
|
||||
|
||||
private val keyGenParameterSpec: KeyGenParameterSpec
|
||||
get() = KeyGenParameterSpec
|
||||
.Builder(
|
||||
ENCRYPTION_KEY_NAME,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setInvalidatedByBiometricEnrollment(true)
|
||||
.build()
|
||||
|
||||
override fun createCipherOrNull(userId: String): Cipher? {
|
||||
val secretKey: SecretKey = generateKeyOrNull(userId = userId)
|
||||
val secretKey: SecretKey = generateKeyOrNull()
|
||||
?: run {
|
||||
// user removed all biometrics from the device
|
||||
destroyBiometrics(userId = userId)
|
||||
@@ -57,25 +68,9 @@ class BiometricsEncryptionManagerImpl(
|
||||
return cipher
|
||||
}
|
||||
|
||||
override fun clearBiometrics(userId: String) {
|
||||
settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState ->
|
||||
settingsDiskSource.storeAccountBiometricIntegrityValidity(
|
||||
userId = userId,
|
||||
systemBioIntegrityState = systemBioIntegrityState,
|
||||
value = null,
|
||||
)
|
||||
}
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
keystore.deleteEntry(encryptionKeyName(userId = userId))
|
||||
}
|
||||
|
||||
override fun getOrCreateCipher(userId: String): Cipher? {
|
||||
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
|
||||
// is available. If neither succeeds, then we need to generate a new one.
|
||||
val secretKey: SecretKey = getSecretKeyOrNull(userId = userId)
|
||||
?: getSecretKeyOrNull(userId = null)
|
||||
?: generateKeyOrNull(userId = userId)
|
||||
val secretKey: SecretKey = getSecretKeyOrNull()
|
||||
?: generateKeyOrNull()
|
||||
?: run {
|
||||
// user removed all biometrics from the device
|
||||
destroyBiometrics(userId = userId)
|
||||
@@ -102,26 +97,11 @@ class BiometricsEncryptionManagerImpl(
|
||||
?: false
|
||||
}
|
||||
|
||||
private fun getKeyGenParameterSpec(userId: String): KeyGenParameterSpec =
|
||||
KeyGenParameterSpec
|
||||
.Builder(
|
||||
encryptionKeyName(userId = userId),
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setInvalidatedByBiometricEnrollment(true)
|
||||
.build()
|
||||
|
||||
private fun encryptionKeyName(userId: String?): String =
|
||||
"${BuildConfig.APPLICATION_ID}.biometric_integrity${userId?.let { ".$it" }.orEmpty()}"
|
||||
|
||||
/**
|
||||
* Generates a [SecretKey] from which the [Cipher] will be generated, or `null` if a key cannot
|
||||
* be generated.
|
||||
*/
|
||||
private fun generateKeyOrNull(userId: String): SecretKey? {
|
||||
private fun generateKeyOrNull(): SecretKey? {
|
||||
val keyGen = try {
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME)
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
@@ -133,7 +113,7 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
|
||||
return try {
|
||||
keyGen.init(getKeyGenParameterSpec(userId = userId))
|
||||
keyGen.init(keyGenParameterSpec)
|
||||
keyGen.generateKey()
|
||||
} catch (_: InvalidAlgorithmParameterException) {
|
||||
null
|
||||
@@ -145,10 +125,10 @@ class BiometricsEncryptionManagerImpl(
|
||||
/**
|
||||
* Returns the [SecretKey] stored in the keystore, or null if there isn't one.
|
||||
*/
|
||||
private fun getSecretKeyOrNull(userId: String?): SecretKey? =
|
||||
private fun getSecretKeyOrNull(): SecretKey? =
|
||||
try {
|
||||
keystore
|
||||
.getKey(encryptionKeyName(userId = userId), null)
|
||||
.getKey(ENCRYPTION_KEY_NAME, null)
|
||||
?.let { it as SecretKey }
|
||||
} catch (_: KeyStoreException) {
|
||||
// keystore was not loaded
|
||||
@@ -192,9 +172,7 @@ class BiometricsEncryptionManagerImpl(
|
||||
* Validates the keystore key and decrypts it using the user-provided [cipher].
|
||||
*/
|
||||
private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean {
|
||||
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
|
||||
// is available.
|
||||
val secretKey = getSecretKeyOrNull(userId = userId) ?: getSecretKeyOrNull(userId = null)
|
||||
val secretKey = getSecretKeyOrNull()
|
||||
return if (cipher != null && secretKey != null) {
|
||||
cipher.initializeCipher(userId = userId, secretKey = secretKey)
|
||||
} else {
|
||||
@@ -219,12 +197,22 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
|
||||
private fun destroyBiometrics(userId: String) {
|
||||
clearBiometrics(userId = userId)
|
||||
settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState ->
|
||||
settingsDiskSource.storeAccountBiometricIntegrityValidity(
|
||||
userId = userId,
|
||||
systemBioIntegrityState = systemBioIntegrityState,
|
||||
value = null,
|
||||
)
|
||||
}
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
keystore.deleteEntry(ENCRYPTION_KEY_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
private const val ENCRYPTION_KEYSTORE_NAME: String = "AndroidKeyStore"
|
||||
private const val ENCRYPTION_KEY_NAME: String = "${BuildConfig.APPLICATION_ID}.biometric_integrity"
|
||||
private const val CIPHER_TRANSFORMATION =
|
||||
KeyProperties.KEY_ALGORITHM_AES + "/" +
|
||||
KeyProperties.BLOCK_MODE_CBC + "/" +
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -40,17 +39,6 @@ interface FirstTimeActionManager {
|
||||
*/
|
||||
val firstTimeStateFlow: Flow<FirstTimeState>
|
||||
|
||||
/**
|
||||
* Returns observable flow of if a user on the device has seen the Add Login coach mark tour.
|
||||
*/
|
||||
val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Returns observable flow of if a user on the device has seen the Generator screen
|
||||
* coach mark tour.
|
||||
*/
|
||||
val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
@@ -78,9 +66,4 @@ interface FirstTimeActionManager {
|
||||
* Update the value of the showImportLoginsSettingsBadge status for the active user.
|
||||
*/
|
||||
fun storeShowImportLoginsSettingsBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Can be called to indicate that a user has seen the AddLogin coach mark tour.
|
||||
*/
|
||||
fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -155,32 +154,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowAddLoginCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureIsEnabled ->
|
||||
// If the feature flag is off always return true so observers know
|
||||
// the card has not been shown.
|
||||
shouldShow && featureIsEnabled
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowGeneratorCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureFlagEnabled ->
|
||||
shouldShow && featureFlagEnabled
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
@@ -238,18 +211,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType) {
|
||||
when (tourCompleted) {
|
||||
CoachMarkTourType.ADD_LOGIN -> {
|
||||
settingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = false)
|
||||
}
|
||||
|
||||
CoachMarkTourType.GENERATOR -> {
|
||||
settingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showImportLogins value which takes
|
||||
* into account if the vault is empty.
|
||||
@@ -296,23 +257,4 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false &&
|
||||
!autofillEnabledManager.isAutofillEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are any existing "Login" type ciphers then we'll map the current value
|
||||
* of the receiver Flow to `false`.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
|
||||
authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
combine(
|
||||
flow = this,
|
||||
flow2 = vaultDiskSource.getCiphers(activeUserId),
|
||||
) { currentValue, ciphers ->
|
||||
currentValue && ciphers.none { it.login != null }
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
|
||||
/**
|
||||
* Primary access point for disk information related to key data.
|
||||
*/
|
||||
interface KeyManager {
|
||||
|
||||
/**
|
||||
* Import a private key into the application KeyStore.
|
||||
*
|
||||
* @param key The private key to be saved.
|
||||
* @param alias Alias to be assigned to the private key.
|
||||
* @param password Password used to protect the certificate.
|
||||
*/
|
||||
fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
password: String,
|
||||
): ImportPrivateKeyResult
|
||||
|
||||
/**
|
||||
* Removes the mTLS key from storage.
|
||||
*/
|
||||
fun removeMutualTlsKey(alias: String, host: MutualTlsKeyHost)
|
||||
|
||||
/**
|
||||
* Retrieve the certificate chain for the selected mTLS key.
|
||||
*
|
||||
* Must be called from a background thread to prevent possible deadlocks on the main thread.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getMutualTlsCertificateChain(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
): MutualTlsCertificate?
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.security.KeyChain
|
||||
import android.security.KeyChainException
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.PrivateKey
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Default implementation of [KeyManager].
|
||||
*/
|
||||
class KeyManagerImpl(
|
||||
private val context: Context,
|
||||
) : KeyManager {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
password: String,
|
||||
): ImportPrivateKeyResult {
|
||||
// Step 1: Load PKCS12 bytes into a KeyStore.
|
||||
val pkcs12KeyStore: KeyStore = key
|
||||
.inputStream()
|
||||
.use { stream ->
|
||||
try {
|
||||
KeyStore.getInstance(KEYSTORE_TYPE_PKCS12)
|
||||
.also { it.load(stream, password.toCharArray()) }
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to load PKCS12 bytes")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
|
||||
return when (e.cause) {
|
||||
is UnrecoverableKeyException -> {
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
}
|
||||
|
||||
else -> {
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
}
|
||||
}
|
||||
} catch (e: CertificateException) {
|
||||
Timber.Forest.e(e, "Unable to load certificate chain")
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Cryptographic algorithm not supported")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Get a list of aliases and choose the first one.
|
||||
val internalAlias = pkcs12KeyStore.aliases()
|
||||
?.takeIf { it.hasMoreElements() }
|
||||
?.nextElement()
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
|
||||
// Step 3: Extract PrivateKey and X.509 certificate from the KeyStore and verify
|
||||
// certificate alias.
|
||||
val privateKey = try {
|
||||
pkcs12KeyStore.getKey(internalAlias, password.toCharArray())
|
||||
?: return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to get private key")
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
}
|
||||
|
||||
val certChain: Array<Certificate> = pkcs12KeyStore
|
||||
.getCertificateChain(internalAlias)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
|
||||
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
|
||||
// does not exists.
|
||||
with(androidKeyStore) {
|
||||
if (containsAlias(alias)) {
|
||||
return ImportPrivateKeyResult.Error.DuplicateAlias
|
||||
}
|
||||
|
||||
try {
|
||||
setKeyEntry(alias, privateKey, null, certChain)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to import key into Android KeyStore")
|
||||
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
}
|
||||
}
|
||||
return ImportPrivateKeyResult.Success(alias)
|
||||
}
|
||||
|
||||
override fun removeMutualTlsKey(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
) {
|
||||
when (host) {
|
||||
MutualTlsKeyHost.ANDROID_KEY_STORE -> removeKeyFromAndroidKeyStore(alias)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMutualTlsCertificateChain(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
): MutualTlsCertificate? = when (host) {
|
||||
MutualTlsKeyHost.ANDROID_KEY_STORE -> getKeyFromAndroidKeyStore(alias)
|
||||
|
||||
MutualTlsKeyHost.KEY_CHAIN -> getSystemKeySpecOrNull(alias)
|
||||
}
|
||||
|
||||
private fun removeKeyFromAndroidKeyStore(alias: String) {
|
||||
try {
|
||||
androidKeyStore.deleteEntry(alias)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to remove key from Android KeyStore")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSystemKeySpecOrNull(alias: String): MutualTlsCertificate? {
|
||||
val systemPrivateKey = try {
|
||||
KeyChain.getPrivateKey(context, alias)
|
||||
} catch (e: KeyChainException) {
|
||||
Timber.Forest.e(e, "Requested alias not found in system KeyChain")
|
||||
null
|
||||
}
|
||||
?: return null
|
||||
|
||||
val systemCertificateChain = try {
|
||||
KeyChain.getCertificateChain(context, alias)
|
||||
} catch (e: KeyChainException) {
|
||||
Timber.Forest.e(e, "Unable to access certificate chain for provided alias")
|
||||
null
|
||||
}
|
||||
?: return null
|
||||
|
||||
return MutualTlsCertificate(
|
||||
alias = alias,
|
||||
certificateChain = systemCertificateChain.toList(),
|
||||
privateKey = systemPrivateKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getKeyFromAndroidKeyStore(alias: String): MutualTlsCertificate? =
|
||||
with(androidKeyStore) {
|
||||
try {
|
||||
val privateKeyRef = (getKey(alias, null) as? PrivateKey)
|
||||
?: return null
|
||||
val certChain = getCertificateChain(alias)
|
||||
.mapNotNull { it as? X509Certificate }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: return null
|
||||
MutualTlsCertificate(
|
||||
alias = alias,
|
||||
certificateChain = certChain,
|
||||
privateKey = privateKeyRef,
|
||||
)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to load Android KeyStore")
|
||||
null
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to load client certificate from Android KeyStore")
|
||||
null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Key cannot be recovered. Password may be incorrect.")
|
||||
null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Algorithm not supported")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val androidKeyStore
|
||||
get() = KeyStore
|
||||
.getInstance(KEYSTORE_TYPE_ANDROID)
|
||||
.also { it.load(null) }
|
||||
}
|
||||
|
||||
private const val KEYSTORE_TYPE_ANDROID = "AndroidKeyStore"
|
||||
private const val KEYSTORE_TYPE_PKCS12 = "pkcs12"
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Responsible for managing the active configuration of the network layer.
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Manager to detect and handle changes to network connectivity.
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
@@ -93,21 +93,13 @@ class PolicyManagerImpl(
|
||||
organization: SyncResponseJson.Profile.Organization,
|
||||
policyType: PolicyTypeJson,
|
||||
): Boolean =
|
||||
when (policyType) {
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
organization.type == OrganizationType.OWNER
|
||||
}
|
||||
|
||||
PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
-> {
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
}
|
||||
if (policyType == PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) {
|
||||
organization.type == OrganizationType.OWNER
|
||||
} else if (policyType == PolicyTypeJson.PASSWORD_GENERATOR) {
|
||||
false
|
||||
} else {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,6 @@ interface BitwardenClipboardManager {
|
||||
toastDescriptorOverride: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* See [setText] for more details.
|
||||
*/
|
||||
fun setText(
|
||||
text: String,
|
||||
isSensitive: Boolean = true,
|
||||
toastDescriptorOverride: Text,
|
||||
)
|
||||
|
||||
/**
|
||||
* See [setText] for more details.
|
||||
*/
|
||||
|
||||
@@ -48,10 +48,7 @@ class BitwardenClipboardManagerImpl(
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
val descriptor = toastDescriptorOverride
|
||||
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
|
||||
?: context.resources.getString(
|
||||
R.string.value_has_been_copied,
|
||||
context.resources.getString(R.string.value),
|
||||
)
|
||||
?: context.resources.getString(R.string.copied_to_clipboard)
|
||||
Toast.makeText(context, descriptor, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -73,14 +70,6 @@ class BitwardenClipboardManagerImpl(
|
||||
setText(text.toAnnotatedString(), isSensitive, toastDescriptorOverride)
|
||||
}
|
||||
|
||||
override fun setText(text: String, isSensitive: Boolean, toastDescriptorOverride: Text) {
|
||||
setText(
|
||||
text.toAnnotatedString(),
|
||||
isSensitive,
|
||||
toastDescriptorOverride.toString(context.resources),
|
||||
)
|
||||
}
|
||||
|
||||
override fun setText(text: Text, isSensitive: Boolean, toastDescriptorOverride: String?) {
|
||||
setText(text.toString(context.resources), isSensitive, toastDescriptorOverride)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterM
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
@@ -30,10 +28,12 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -54,10 +54,6 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
|
||||
@@ -68,7 +64,6 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -334,28 +329,4 @@ object PlatformManagerModule {
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyManager(
|
||||
@ApplicationContext context: Context,
|
||||
): KeyManager = KeyManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppResumeManager(
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
authRepository: AuthRepository,
|
||||
vaultLockManager: VaultLockManager,
|
||||
clock: Clock,
|
||||
): AppResumeManager {
|
||||
return AppResumeManagerImpl(
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Data class representing the Screen Data for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class AppResumeScreenData {
|
||||
|
||||
/**
|
||||
* Data object representing the Generator screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object GeneratorScreen : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data object representing the Send screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object SendScreen : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data class representing the Search screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data class SearchScreen(val searchTerm: String) : AppResumeScreenData()
|
||||
|
||||
/**
|
||||
* Data object representing the Verification Code screen for app resume.
|
||||
*/
|
||||
@Serializable
|
||||
data object VerificationCodeScreen : AppResumeScreenData()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Enumerated values to represent all the possible coach mark tours that can be
|
||||
* completed.
|
||||
*
|
||||
* @see com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
*/
|
||||
enum class CoachMarkTourType {
|
||||
ADD_LOGIN,
|
||||
GENERATOR,
|
||||
}
|
||||
@@ -39,12 +39,6 @@ sealed class FlagKey<out T : Any> {
|
||||
NewDevicePermanentDismiss,
|
||||
NewDeviceTemporaryDismiss,
|
||||
IgnoreEnvironmentCheck,
|
||||
MutualTls,
|
||||
SingleTapPasskeyCreation,
|
||||
SingleTapPasskeyAuthentication,
|
||||
AnonAddySelfHostAlias,
|
||||
SimpleLoginSelfHostAlias,
|
||||
ChromeAutofill,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,7 +47,7 @@ sealed class FlagKey<out T : Any> {
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-bwa-sync"
|
||||
override val keyName: String = "enable-authenticator-sync-android"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
@@ -73,7 +67,7 @@ sealed class FlagKey<out T : Any> {
|
||||
data object OnboardingCarousel : FlagKey<Boolean>() {
|
||||
override val keyName: String = "native-carousel-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +76,7 @@ sealed class FlagKey<out T : Any> {
|
||||
data object OnboardingFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "native-create-account-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +85,7 @@ sealed class FlagKey<out T : Any> {
|
||||
data object ImportLoginsFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "import-logins-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +140,7 @@ sealed class FlagKey<out T : Any> {
|
||||
*/
|
||||
data object CipherKeyEncryption : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cipher-key-encryption"
|
||||
override val defaultValue: Boolean = false
|
||||
override val defaultValue: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
@@ -177,62 +171,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Mutual TLS feature.
|
||||
*/
|
||||
data object MutualTls : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mutual-tls"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable single tap passkey creation.
|
||||
*/
|
||||
data object SingleTapPasskeyCreation : FlagKey<Boolean>() {
|
||||
override val keyName: String = "single-tap-passkey-creation"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable single tap passkey authentication.
|
||||
*/
|
||||
data object SingleTapPasskeyAuthentication : FlagKey<Boolean>() {
|
||||
override val keyName: String = "single-tap-passkey-authentication"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable AnonAddy (addy.io) self host alias
|
||||
* generation.
|
||||
*/
|
||||
data object AnonAddySelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "anon-addy-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable SimpleLogin self-host alias generation.
|
||||
*/
|
||||
data object SimpleLoginSelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "simple-login-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the checking for Chrome's third party
|
||||
* autofill.
|
||||
*/
|
||||
data object ChromeAutofill : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-chrome-autofill"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Models the result of importing a private key.
|
||||
*/
|
||||
sealed class ImportPrivateKeyResult {
|
||||
|
||||
/**
|
||||
* Represents a successful result of importing a private key.
|
||||
*
|
||||
* @property alias The alias assigned to the imported private key.
|
||||
*/
|
||||
data class Success(val alias: String) : ImportPrivateKeyResult()
|
||||
|
||||
/**
|
||||
* Represents a generic error during the import process.
|
||||
*/
|
||||
sealed class Error : ImportPrivateKeyResult() {
|
||||
|
||||
/**
|
||||
* Indicates that the provided key is unrecoverable or the password is incorrect.
|
||||
*/
|
||||
data object UnrecoverableKey : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the certificate chain associated with the key is invalid.
|
||||
*/
|
||||
data object InvalidCertificateChain : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the specified alias is already in use.
|
||||
*/
|
||||
data object DuplicateAlias : Error()
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred during the key store operation.
|
||||
*/
|
||||
data object KeyStoreOperationFailed : Error()
|
||||
|
||||
/**
|
||||
* Indicates the provided key is not supported.
|
||||
*/
|
||||
data object UnsupportedKey : Error()
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,5 @@ enum class NotificationType {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class NotificationTypeSerializer : BaseEnumeratedIntSerializer<NotificationType>(
|
||||
className = "NotificationType",
|
||||
values = NotificationType.entries.toTypedArray(),
|
||||
)
|
||||
private class NotificationTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<NotificationType>(NotificationType.entries.toTypedArray())
|
||||
|
||||
@@ -130,6 +130,5 @@ enum class OrganizationEventType {
|
||||
|
||||
@Keep
|
||||
private class OrganizationEventTypeSerializer : BaseEnumeratedIntSerializer<OrganizationEventType>(
|
||||
className = "OrganizationEventType",
|
||||
values = OrganizationEventType.entries.toTypedArray(),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user