mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
Compare commits
8 Commits
agalles/te
...
agalles/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b745991a | ||
|
|
302602a933 | ||
|
|
a79634e34b | ||
|
|
f77839e758 | ||
|
|
a0874a7c4f | ||
|
|
aa9e151a0b | ||
|
|
df2c7039b2 | ||
|
|
44fdbec92c |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -48,9 +48,3 @@
|
||||
# app/src/main/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
|
||||
# app/src/test/java/com/x8bit/bitwarden/data/vault @bitwarden/team-vault-dev
|
||||
# app/src/test/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
|
||||
|
||||
# Docker-related files
|
||||
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
20
.github/actions/log-inputs/action.yml
vendored
20
.github/actions/log-inputs/action.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'Log Inputs to Job Summary'
|
||||
description: 'Log workflow inputs to the GitHub Actions job summary'
|
||||
|
||||
inputs:
|
||||
inputs:
|
||||
description: 'Workflow inputs as JSON'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
shell: bash
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
49
.github/actions/setup-android-build/action.yml
vendored
49
.github/actions/setup-android-build/action.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: 'Setup Android Build'
|
||||
description: 'Setup Android build environment with Gradle, Ruby, and Fastlane'
|
||||
inputs:
|
||||
java-version:
|
||||
description: 'Java version to use'
|
||||
required: false
|
||||
default: '21'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
- name: Install Fastlane
|
||||
shell: bash
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -27,9 +27,6 @@
|
||||
],
|
||||
"matchManagers": [
|
||||
"gradle"
|
||||
],
|
||||
"excludePackageNames": [
|
||||
"com.github.bumptech.glide:compose"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
36
.github/workflows/build-authenticator.yml
vendored
36
.github/workflows/build-authenticator.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -28,7 +27,7 @@ on:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 21
|
||||
JAVA_VERSION: 17
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -51,13 +50,13 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -67,7 +66,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -76,13 +75,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -110,10 +109,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -189,10 +188,10 @@ jobs:
|
||||
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -202,7 +201,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -211,20 +210,11 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
|
||||
|
||||
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -18,17 +17,17 @@ on:
|
||||
distribute-to-firebase:
|
||||
description: "Optional. Distribute artifacts to Firebase."
|
||||
required: false
|
||||
default: true
|
||||
default: false
|
||||
type: boolean
|
||||
publish-to-play-store:
|
||||
description: "Optional. Deploy bundle artifact to Google Play Store"
|
||||
required: false
|
||||
default: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 21
|
||||
JAVA_VERSION: 17
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
@@ -52,13 +51,13 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -68,7 +67,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -77,13 +76,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -118,10 +117,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -184,10 +183,10 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -197,7 +196,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -206,7 +205,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -417,7 +416,7 @@ jobs:
|
||||
bundle exec fastlane run validate_play_store_json_key
|
||||
|
||||
- name: Publish Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
||||
run: |
|
||||
bundle exec fastlane publishProdToPlayStore
|
||||
bundle exec fastlane publishBetaToPlayStore
|
||||
@@ -429,10 +428,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -481,10 +480,10 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -494,7 +493,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -503,7 +502,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
|
||||
|
||||
- name: Download Google Privileged Browsers List
|
||||
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
|
||||
|
||||
6
.github/workflows/crowdin-pull.yml
vendored
6
.github/workflows/crowdin-pull.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -43,14 +43,14 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@9fd07c1c5b36b15f082d1d860dc399f16f849bd7 # v2.9.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
4
.github/workflows/crowdin-push.yml
vendored
4
.github/workflows/crowdin-push.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@9fd07c1c5b36b15f082d1d860dc399f16f849bd7 # v2.9.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
78
.github/workflows/github-release.yml
vendored
78
.github/workflows/github-release.yml
vendored
@@ -25,15 +25,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Get branch from workflow run
|
||||
id: get_release_branch
|
||||
env:
|
||||
@@ -50,29 +45,23 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔖 Release branch: $release_branch"
|
||||
echo "🔖 Workflow name: $workflow_name"
|
||||
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
|
||||
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
|
||||
|
||||
case "$workflow_name" in
|
||||
*"Password Manager"* | "Build")
|
||||
app_name="Password Manager"
|
||||
app_name_suffix="bwpm"
|
||||
echo "app_name=Password Manager" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=bwpm" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*"Authenticator"*)
|
||||
app_name="Authenticator"
|
||||
app_name_suffix="bwa"
|
||||
echo "app_name=Authenticator" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=bwa" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown workflow name: $workflow_name"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "🔖 App name: $app_name"
|
||||
echo "🔖 App name suffix: $app_name_suffix"
|
||||
echo "app_name=$app_name" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get version info from run logs and set release tag name
|
||||
id: get_release_info
|
||||
@@ -110,7 +99,7 @@ jobs:
|
||||
echo "🔖 New tag name: $tag_name"
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
|
||||
last_release_tag=$(git tag -l --sort=-authordate | grep "$_APP_NAME_SUFFIX" | head -n 1)
|
||||
last_release_tag=$(git tag -l --sort=-authordate | grep "$APP_NAME_SUFFIX" | head -n 1)
|
||||
echo "🔖 Last release tag: $last_release_tag"
|
||||
echo "last_release_tag=$last_release_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -127,34 +116,6 @@ jobs:
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
# Files that won't be included in any release
|
||||
files_to_remove=(
|
||||
"com.x8bit.bitwarden.aab"
|
||||
"com.x8bit.bitwarden.aab-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.beta.apk"
|
||||
"com.x8bit.bitwarden.beta.apk-sha256.txt"
|
||||
"com.x8bit.bitwarden.beta.aab"
|
||||
"com.x8bit.bitwarden.beta.aab-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk"
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.dev.apk"
|
||||
"com.x8bit.bitwarden.dev.apk-sha256.txt"
|
||||
|
||||
"com.bitwarden.authenticator.aab"
|
||||
"authenticator-android-aab-sha256.txt"
|
||||
)
|
||||
|
||||
for file in "${files_to_remove[@]}"; do
|
||||
find $ARTIFACTS_PATH -name "$file" -type f -delete
|
||||
done
|
||||
echo "🔖 Removed internal artifacts."
|
||||
echo ""
|
||||
echo "🔖 Files to be included in the release:"
|
||||
find $ARTIFACTS_PATH -type f
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
@@ -206,28 +167,23 @@ jobs:
|
||||
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
|
||||
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
|
||||
run: |
|
||||
is_latest_release=false
|
||||
if [[ "$_APP_NAME" == "Password Manager" ]]; then
|
||||
is_latest_release=true
|
||||
fi
|
||||
|
||||
echo "⌛️ Creating release for $_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER) on $_TARGET_COMMIT"
|
||||
release_url=$(gh release create "$_TAG_NAME" \
|
||||
--title "$_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER)" \
|
||||
--target "$_TARGET_COMMIT" \
|
||||
--generate-notes \
|
||||
--notes-start-tag "$_LAST_RELEASE_TAG" \
|
||||
--latest=$is_latest_release \
|
||||
--draft \
|
||||
$ARTIFACTS_PATH/*/*)
|
||||
|
||||
# Extract release tag from URL
|
||||
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
|
||||
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
|
||||
echo "url=$release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ Release created: $release_url"
|
||||
echo "🔖 Release ID from URL: $release_id_from_url"
|
||||
|
||||
# Get release info for outputs
|
||||
release_data=$(gh release view "$_TAG_NAME" --json id)
|
||||
release_id=$(echo "$release_data" | jq -r .id)
|
||||
|
||||
echo "id=$release_id" >> $GITHUB_OUTPUT
|
||||
echo "url=$release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update Release Description
|
||||
id: update_release_description
|
||||
@@ -235,10 +191,10 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
|
||||
_RELEASE_ID: ${{ steps.create_release.outputs.release_id_from_url }}
|
||||
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
|
||||
run: |
|
||||
echo "Getting current release body. Release ID: $_RELEASE_ID"
|
||||
current_body=$(gh release view "$_RELEASE_ID" --json body --jq .body)
|
||||
echo "Getting current release body. Tag: $_TAG_NAME"
|
||||
current_body=$(gh release view "$_TAG_NAME" --json body --jq .body)
|
||||
|
||||
product_release_notes=$(cat product_release_notes.txt)
|
||||
|
||||
@@ -249,7 +205,7 @@ jobs:
|
||||
${current_body}
|
||||
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
|
||||
|
||||
new_release_url=$(gh release edit "$_RELEASE_ID" --notes "$updated_body")
|
||||
new_release_url=$(gh release edit "$_TAG_NAME" --notes "$updated_body")
|
||||
|
||||
# draft release links change after editing
|
||||
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release-bwpm.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
@@ -3,7 +3,7 @@ name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
|
||||
- cron: '0 3 * * 1-5'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -11,12 +11,24 @@ permissions:
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
|
||||
publish-release-authenticator:
|
||||
name: Publish Authenticator Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Authenticator"
|
||||
workflow_name: "publish-github-release-bwa.yml"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "authenticator_play_store-creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
30
.github/workflows/publish-store.yml
vendored
30
.github/workflows/publish-store.yml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Publish to Google Play
|
||||
run-name: >
|
||||
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -47,10 +46,6 @@ on:
|
||||
- production
|
||||
- Fastlane Automation Target
|
||||
required: true
|
||||
dry-run:
|
||||
description: "Dry-Run, Run the workflow without publishing to the store"
|
||||
type: boolean
|
||||
default: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
@@ -59,7 +54,6 @@ permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
@@ -77,10 +71,10 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -128,8 +122,6 @@ jobs:
|
||||
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Promote Play Store version to production
|
||||
if: ${{ inputs.dry-run == false }}
|
||||
id: publish
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
@@ -166,19 +158,3 @@ jobs:
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
track:"$TRACK_FROM" \
|
||||
trackPromoteTo:"$TRACK_TARGET"
|
||||
|
||||
- name: Enable Publish Github Release Workflow
|
||||
if: ${{ steps.publish.conclusion == 'success' || inputs.dry-run }}
|
||||
env:
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
run: |
|
||||
if $DRY_RUN ; then
|
||||
gh workflow view publish-github-release.yml
|
||||
exit 0
|
||||
fi
|
||||
if [ "$PRODUCT" = "Password Manager" ]; then
|
||||
gh workflow enable publish-github-release-bwpm.yml
|
||||
elif [ "$PRODUCT" = "Authenticator" ]; then
|
||||
gh workflow enable publish-github-release-bwa.yml
|
||||
fi
|
||||
|
||||
53
.github/workflows/release-branch.yml
vendored
53
.github/workflows/release-branch.yml
vendored
@@ -9,9 +9,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- RC
|
||||
- Hotfix Password Manager
|
||||
- Hotfix Authenticator
|
||||
- Test
|
||||
- Hotfix
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
@@ -19,54 +17,38 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC or Test Branch
|
||||
id: rc_branch
|
||||
if: inputs.release_type == 'RC' || inputs.release_type == 'Test'
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
env:
|
||||
_TEST_MODE: ${{ inputs.release_type == 'Test' }}
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
run: |
|
||||
current_date=$(date +'%Y.%-m')
|
||||
branch_name="${current_date}-rc${{ github.run_number }}"
|
||||
|
||||
if [ "$_TEST_MODE" = "true" ]; then
|
||||
branch_name="WORKFLOW-TEST-${branch_name}"
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
branch_name="release/${current_date}-rc${{ github.run_number }}"
|
||||
else
|
||||
branch_name="release/rc${{ github.run_number }}"
|
||||
fi
|
||||
branch_name="release/${branch_name}"
|
||||
|
||||
git switch main
|
||||
git switch -c $branch_name
|
||||
git push origin $branch_name
|
||||
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Create Hotfix Branch
|
||||
id: hotfix_branch
|
||||
if: startsWith(inputs.release_type, 'Hotfix')
|
||||
env:
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
run: |
|
||||
app_codename="bwpm"
|
||||
if [ "$_RELEASE_TYPE" == "Hotfix Authenticator" ]; then
|
||||
app_codename="bwa"
|
||||
fi
|
||||
echo "🌿 app codename: $app_codename"
|
||||
|
||||
latest_tag=$(git tag -l --sort=-creatordate | grep "$app_codename" | head -n 1)
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "::error::No tags found in the repository"
|
||||
exit 1
|
||||
fi
|
||||
branch_name="release/hotfix-${latest_tag}"
|
||||
echo "🌿 branch name: $branch_name"
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
|
||||
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
@@ -74,12 +56,3 @@ jobs:
|
||||
git switch -c $branch_name $latest_tag
|
||||
git push origin $branch_name
|
||||
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Trigger CI Workflows
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "🌿 branch name: $_BRANCH_NAME"
|
||||
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
|
||||
8
.github/workflows/scan-ci.yml
vendored
8
.github/workflows/scan-ci.yml
vendored
@@ -9,9 +9,16 @@ on:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
sast:
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -25,6 +32,7 @@ jobs:
|
||||
quality:
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
|
||||
227
.github/workflows/sdlc-sdk-update.yml
vendored
227
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -1,227 +0,0 @@
|
||||
name: SDLC / SDK Update
|
||||
run-name: "SDK ${{inputs.run-mode == 'Update' && format('Update - {0}', inputs.sdk-version) || format('Test #{0} - {1}', inputs.pr-id, inputs.sdk-version)}}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-mode:
|
||||
description: "Run Mode"
|
||||
type: choice
|
||||
options:
|
||||
- Test # used for testing sdk-internal repo PRs
|
||||
- Update # opens a PR in this repo updating the SDK
|
||||
default: Test
|
||||
sdk-package:
|
||||
description: "SDK Package ID"
|
||||
required: true
|
||||
default: "com.bitwarden:sdk-android.dev"
|
||||
sdk-version:
|
||||
description: "SDK Version"
|
||||
required: true
|
||||
default: "1.0.0-2686-km-update-kdf-sdk"
|
||||
pr-id:
|
||||
description: "Pull Request ID"
|
||||
|
||||
env:
|
||||
_BOT_NAME: "bw-ghapp[bot]"
|
||||
_BOT_EMAIL: "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update and PR
|
||||
if: ${{ inputs.run-mode == 'Update' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-pull-requests: write
|
||||
permission-actions: read
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Switch to branch
|
||||
id: switch-branch
|
||||
run: |
|
||||
BRANCH_NAME="sdlc/sdk-update"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
if git switch $BRANCH_NAME; then
|
||||
echo "✅ Switched to existing branch: $BRANCH_NAME"
|
||||
echo "updating_existing_branch=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📝 Creating new branch: $BRANCH_NAME"
|
||||
git switch -c $BRANCH_NAME
|
||||
echo "updating_existing_branch=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prevent updating the branch when the last committer isn't the bot
|
||||
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' $_BRANCH_NAME)
|
||||
|
||||
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
|
||||
echo "Expected bot email: $_BOT_EMAIL"
|
||||
|
||||
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
|
||||
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
|
||||
"This indicates manual changes have been made to the branch," \
|
||||
"PR has to be merged or closed before running this workflow again."
|
||||
echo "👀 Fetching existing PR..."
|
||||
gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty'
|
||||
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ❌ Merge or close: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
|
||||
|
||||
# Using main to retrieve the changelog on consecutive updates of the same PR.
|
||||
- name: Get current SDK version from main branch
|
||||
id: get-current-sdk
|
||||
run: |
|
||||
git show origin/main:gradle/libs.versions.toml
|
||||
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
|
||||
if [ -z "$SDK_VERSION" ]; then
|
||||
echo "::error::Failed to get current SDK version from main branch."
|
||||
exit 1
|
||||
fi
|
||||
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
|
||||
echo "Current SDK version (from main): $SDK_VERSION"
|
||||
echo "Current SDK git ref: $GIT_REF"
|
||||
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Create branch and commit
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "👀 Committing SDK version update..."
|
||||
|
||||
git config user.name "$_BOT_NAME"
|
||||
git config user.email "$_BOT_EMAIL"
|
||||
|
||||
git add gradle/libs.versions.toml
|
||||
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
|
||||
git push origin $_BRANCH_NAME
|
||||
|
||||
- name: Create or Update Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_OLD_SDK_VERSION: ${{ steps.get-current-sdk.outputs.version }}
|
||||
_OLD_SDK_GIT_REF: ${{ steps.get-current-sdk.outputs.git_ref }}
|
||||
run: |
|
||||
NEW_SDK_GIT_REF=$(echo "$_SDK_VERSION" | cut -d'-' -f3-)
|
||||
CHANGELOG=$(./scripts/get-repo-changelog.sh "bitwarden/sdk-internal" "$_OLD_SDK_GIT_REF" "$NEW_SDK_GIT_REF")
|
||||
PR_BODY="Updates the SDK version from \`$_OLD_SDK_VERSION\` to \`$_SDK_PACKAGE $_SDK_VERSION\`
|
||||
|
||||
## What's Changed
|
||||
|
||||
$CHANGELOG"
|
||||
|
||||
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "🔄 Updating existing PR #$EXISTING_PR..."
|
||||
echo -e "$PR_BODY" | gh pr edit $EXISTING_PR \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file -
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ✅ Updated PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "📝 Creating new PR..."
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head $_BRANCH_NAME \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Test Update
|
||||
if: ${{ inputs.run-mode == 'Test' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Setup Android Build
|
||||
uses: ./.github/actions/setup-android-build
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
|
||||
run: |
|
||||
./gradlew assembleDebug --warn
|
||||
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
_JAVA_VERSION: 21
|
||||
_JAVA_VERSION: 17
|
||||
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -52,12 +52,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@bb6434c747fa7022e12fa1cae2a0951fcffcff26 # v1.253.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env._JAVA_VERSION }}
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,13 +3,6 @@
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
|
||||
# Ruby / Bundler
|
||||
.bundle/
|
||||
vendor/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -11,8 +11,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1166.0)
|
||||
aws-sdk-core (3.233.0)
|
||||
aws-partitions (1.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -20,18 +20,18 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.231.0)
|
||||
aws-sdk-kms (1.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.199.1)
|
||||
aws-sdk-core (~> 3, >= 3.231.0)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.3)
|
||||
bigdecimal (3.2.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -169,7 +169,7 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.15.0)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -192,15 +192,15 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.4)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.21.0)
|
||||
signet (0.20.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
|
||||
37
README.md
37
README.md
@@ -52,47 +52,16 @@
|
||||
|
||||
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
|
||||
|
||||
4. Setup JDK `Version` `21`:
|
||||
4. Setup JDK `Version` `17`:
|
||||
|
||||
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
|
||||
- Hit the selected Gradle JDK next to `Gradle JDK:`.
|
||||
- Select a `21.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `21`.
|
||||
- Select a `17.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `17`.
|
||||
- Select your preferred `Vendor`.
|
||||
- Hit `Download`.
|
||||
- Hit `Apply`.
|
||||
|
||||
5. Setup `detekt` pre-commit hook (optional):
|
||||
|
||||
Run the following script from the root of the repository to install the hook. This will overwrite any existing pre-commit hook if present.
|
||||
|
||||
```shell
|
||||
echo "Writing detekt pre-commit hook..."
|
||||
cat << 'EOL' > .git/hooks/pre-commit
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Running detekt check..."
|
||||
OUTPUT="/tmp/detekt-$(date +%s)"
|
||||
./gradlew -Pprecommit=true detekt > $OUTPUT
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
cat $OUTPUT
|
||||
rm $OUTPUT
|
||||
echo "***********************************************"
|
||||
echo " detekt failed "
|
||||
echo " Please fix the above issues before committing "
|
||||
echo "***********************************************"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
rm $OUTPUT
|
||||
EOL
|
||||
echo "detekt pre-commit hook written to .git/hooks/pre-commit"
|
||||
echo "Making the hook executable"
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
|
||||
```
|
||||
|
||||
## Theme
|
||||
|
||||
### Icons & Illustrations
|
||||
|
||||
@@ -224,7 +224,6 @@ dependencies {
|
||||
|
||||
implementation(project(":annotation"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":cxf"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":ui"))
|
||||
@@ -246,8 +245,6 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.providerevents)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
@@ -261,6 +258,7 @@ dependencies {
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.bitwarden.sdk)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.google.hilt.android)
|
||||
ksp(libs.google.hilt.compiler)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
@@ -330,7 +328,6 @@ private fun renameFile(path: String, newName: String) {
|
||||
if (originalFile.renameTo(newFile)) {
|
||||
println("Renamed $originalFile to $newFile")
|
||||
} else {
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
throw RuntimeException("Failed to rename $originalFile to $newFile")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "11387825dab701f9d2dd2e940ffbd794",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasTotp",
|
||||
"columnName": "has_totp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, `default_user_collection_email` TEXT, `type` TEXT NOT NULL DEFAULT '0', PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUserCollectionEmail",
|
||||
"columnName": "default_user_collection_email",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'0'"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11387825dab701f9d2dd2e940ffbd794')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,6 @@
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Handle Credential Exchange transfer requests -->
|
||||
<intent-filter
|
||||
android:autoVerify="true"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="application/octet-stream"
|
||||
android:scheme="content"
|
||||
tools:ignore="AppLinkUriRelativeFilterGroupError" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -115,11 +115,11 @@
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".AutofillCallbackActivity"
|
||||
android:name=".AutofillTotpCopyActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/AutofillCallbackTheme" />
|
||||
android:theme="@style/AutofillTotpCopyTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".AuthCallbackActivity"
|
||||
@@ -133,6 +133,16 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="captcha-callback"
|
||||
android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="duo-callback"
|
||||
android:scheme="bitwarden" />
|
||||
@@ -249,7 +259,7 @@
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill_title"
|
||||
android:label="@string/autofill"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@@ -26,6 +27,7 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
|
||||
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
|
||||
val webAuthResult = action.intent.getWebAuthResultOrNull()
|
||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
when {
|
||||
@@ -33,6 +35,12 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
|
||||
}
|
||||
|
||||
captchaCallbackTokenResult != null -> {
|
||||
authRepository.setCaptchaCallbackTokenResult(
|
||||
tokenResult = captchaCallbackTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
duoCallbackTokenResult != null -> {
|
||||
authRepository.setDuoCallbackTokenResult(
|
||||
tokenResult = duoCallbackTokenResult,
|
||||
|
||||
@@ -15,18 +15,18 @@ import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* An activity that is launched to complete Autofill. This is done when an autofill item is selected
|
||||
* and is associated with a valid cipher. Due to the constraints of the autofill framework, we also
|
||||
* have to re-fulfill the autofill for the views that are being filled.
|
||||
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
|
||||
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
|
||||
* we also have to re-fulfill the autofill for the views that are being filled.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class AutofillCallbackActivity : AppCompatActivity() {
|
||||
class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
private val viewModel: AutofillCallbackViewModel by viewModels()
|
||||
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
@@ -34,7 +34,11 @@ class AutofillCallbackActivity : AppCompatActivity() {
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
viewModel.trySendAction(AutofillCallbackAction.IntentReceived(intent = intent))
|
||||
autofillTotpCopyViewModel.trySendAction(
|
||||
AutofillTotpCopyAction.IntentReceived(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
@@ -46,12 +50,17 @@ class AutofillCallbackActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
viewModel
|
||||
autofillTotpCopyViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is AutofillCallbackEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
is AutofillCallbackEvent.FinishActivity -> finishActivity()
|
||||
is AutofillTotpCopyEvent.CompleteAutofill -> {
|
||||
handleCompleteAutofill(event)
|
||||
}
|
||||
|
||||
is AutofillTotpCopyEvent.FinishActivity -> {
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -60,7 +69,7 @@ class AutofillCallbackActivity : AppCompatActivity() {
|
||||
/**
|
||||
* Complete autofill with the provided data.
|
||||
*/
|
||||
private fun handleCompleteAutofill(event: AutofillCallbackEvent.CompleteAutofill) {
|
||||
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillCallbackIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -22,63 +21,45 @@ import javax.inject.Inject
|
||||
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
|
||||
|
||||
/**
|
||||
* A view model that handles logic for the [AutofillCallbackActivity].
|
||||
* A view model that handles logic for the [AutofillTotpCopyActivity].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AutofillCallbackViewModel @Inject constructor(
|
||||
class AutofillTotpCopyViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<Unit, AutofillCallbackEvent, AutofillCallbackAction>(Unit) {
|
||||
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
override fun handleAction(action: AutofillCallbackAction): Unit = when (action) {
|
||||
is AutofillCallbackAction.IntentReceived -> handleIntentReceived(action)
|
||||
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
|
||||
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received intent and alert the activity of what to do next.
|
||||
*/
|
||||
private fun handleIntentReceived(action: AutofillCallbackAction.IntentReceived) {
|
||||
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
|
||||
viewModelScope
|
||||
.launchWithTimeout(
|
||||
timeoutBlock = {
|
||||
Timber.w("Autofill -- Timeout")
|
||||
finishActivity()
|
||||
},
|
||||
timeoutBlock = { finishActivity() },
|
||||
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
|
||||
) {
|
||||
// Extract TOTP copy data from the intent.
|
||||
val cipherId = action
|
||||
.intent
|
||||
.getAutofillCallbackIntentOrNull()
|
||||
.getTotpCopyIntentOrNull()
|
||||
?.cipherId
|
||||
|
||||
if (cipherId == null) {
|
||||
Timber.w("Autofill -- Cipher was not provided")
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
if (isVaultLocked()) {
|
||||
Timber.w("Autofill -- Vault is locked")
|
||||
if (cipherId == null || isVaultLocked()) {
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.w("Autofill -- Cipher not found")
|
||||
finishActivity()
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.w(result.error, "Autofill -- Get cipher failure")
|
||||
finishActivity()
|
||||
}
|
||||
|
||||
GetCipherResult.CipherNotFound -> finishActivity()
|
||||
is GetCipherResult.Failure -> finishActivity()
|
||||
is GetCipherResult.Success -> {
|
||||
Timber.d("Autofill -- Cipher found")
|
||||
sendEvent(AutofillCallbackEvent.CompleteAutofill(result.cipherView))
|
||||
sendEvent(AutofillTotpCopyEvent.CompleteAutofill(result.cipherView))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +69,7 @@ class AutofillCallbackViewModel @Inject constructor(
|
||||
* Send an event to the activity that signals it to finish.
|
||||
*/
|
||||
private fun finishActivity() {
|
||||
sendEvent(AutofillCallbackEvent.FinishActivity)
|
||||
sendEvent(AutofillTotpCopyEvent.FinishActivity)
|
||||
}
|
||||
|
||||
private suspend fun isVaultLocked(): Boolean {
|
||||
@@ -105,30 +86,30 @@ class AutofillCallbackViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions that can be sent to the [AutofillCallbackViewModel].
|
||||
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillCallbackAction {
|
||||
sealed class AutofillTotpCopyAction {
|
||||
/**
|
||||
* An [intent] has been received and is ready to be processed.
|
||||
*/
|
||||
data class IntentReceived(
|
||||
val intent: Intent,
|
||||
) : AutofillCallbackAction()
|
||||
) : AutofillTotpCopyAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events emitted by the [AutofillCallbackViewModel].
|
||||
* Represents events emitted by the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillCallbackEvent {
|
||||
sealed class AutofillTotpCopyEvent {
|
||||
/**
|
||||
* Complete autofill with the provided [cipherView].
|
||||
*/
|
||||
data class CompleteAutofill(
|
||||
val cipherView: CipherView,
|
||||
) : AutofillCallbackEvent()
|
||||
) : AutofillTotpCopyEvent()
|
||||
|
||||
/**
|
||||
* Finish the activity.
|
||||
*/
|
||||
data object FinishActivity : AutofillCallbackEvent()
|
||||
data object FinishActivity : AutofillTotpCopyEvent()
|
||||
}
|
||||
@@ -8,7 +8,6 @@ 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.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -39,17 +38,6 @@ class BitwardenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var restrictionManager: RestrictionManager
|
||||
|
||||
@Inject
|
||||
lateinit var environmentRepository: EnvironmentRepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// These must be initialized in order to ensure that the restrictionManager does not
|
||||
// override the environmentRepository values.
|
||||
restrictionManager.initialize()
|
||||
environmentRepository.initialize()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Timber.w("onLowMemory")
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -43,6 +44,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ANDROID_15_BUG_MAX_REVISION: Int = 241007
|
||||
|
||||
/**
|
||||
* Primary entry point for the application.
|
||||
*/
|
||||
@@ -183,6 +186,12 @@ class MainActivity : AppCompatActivity() {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(baseContext, event.message.invoke(resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
@@ -215,7 +224,35 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun handleRecreate() {
|
||||
ActivityCompat.recreate(this)
|
||||
val isOldAndroidBuildRevision = {
|
||||
// This fetches the date portion of the ID in order to determine the revision of
|
||||
// Android 15 being used and whether we want to use the `recreate` API or not.
|
||||
// If we fail to parse a date, we assume it is not an old revision.
|
||||
"\\.([^.]+)\\."
|
||||
.toRegex()
|
||||
.find(Build.ID)
|
||||
?.groups
|
||||
?.get(1)
|
||||
?.value
|
||||
?.toIntOrNull()
|
||||
?.let { it <= ANDROID_15_BUG_MAX_REVISION } == true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM &&
|
||||
isOldAndroidBuildRevision()
|
||||
) {
|
||||
// This is done to avoid a bug in specific older revisions of Android 15. The bug has
|
||||
// been fixed but certain phones that are no longer supported will never get the fix.
|
||||
// The OS bug is tracked here: https://issuetracker.google.com/issues/370180732
|
||||
startActivity(
|
||||
Intent
|
||||
.makeMainActivity(componentName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION),
|
||||
)
|
||||
finish()
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
|
||||
} else {
|
||||
ActivityCompat.recreate(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {
|
||||
|
||||
@@ -4,13 +4,11 @@ import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -38,6 +36,7 @@ import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticato
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
@@ -85,7 +84,6 @@ class MainViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
private val clock: Clock,
|
||||
private val toastManager: ToastManager,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
@@ -297,7 +295,6 @@ class MainViewModel @Inject constructor(
|
||||
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
|
||||
val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
@@ -421,16 +418,6 @@ class MainViewModel @Inject constructor(
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
|
||||
importCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CredentialExchangeExport(
|
||||
data = ImportCredentialsRequestData(
|
||||
uri = importCredentialsRequest.uri,
|
||||
requestJson = importCredentialsRequest.request.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,15 +433,16 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
when (emailTokenResult) {
|
||||
is EmailTokenResult.Error -> {
|
||||
emailTokenResult
|
||||
.message
|
||||
?.let { toastManager.show(message = it) }
|
||||
?: run {
|
||||
toastManager.show(
|
||||
messageId = BitwardenString
|
||||
.there_was_an_issue_validating_the_registration_token,
|
||||
)
|
||||
}
|
||||
sendEvent(
|
||||
MainEvent.ShowToast(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: BitwardenString
|
||||
.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
EmailTokenResult.Expired -> {
|
||||
@@ -597,6 +585,11 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.bitwarden.network.model.AccountKeysJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
@@ -127,34 +126,13 @@ interface AuthDiskSource : AppIdProvider {
|
||||
/**
|
||||
* Retrieves a private key using a [userId].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Use getAccountKeys instead.",
|
||||
replaceWith = ReplaceWith("getAccountKeys"),
|
||||
)
|
||||
fun getPrivateKey(userId: String): String?
|
||||
|
||||
/**
|
||||
* Stores a private key using a [userId].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Use storeAccountKeys instead.",
|
||||
replaceWith = ReplaceWith("storeAccountKeys"),
|
||||
)
|
||||
fun storePrivateKey(userId: String, privateKey: String?)
|
||||
|
||||
/**
|
||||
* Returns the profile account keys for the given [userId].
|
||||
*/
|
||||
fun getAccountKeys(userId: String): AccountKeysJson?
|
||||
|
||||
/**
|
||||
* Stores the profile account keys for the given [userId].
|
||||
*/
|
||||
fun storeAccountKeys(
|
||||
userId: String,
|
||||
accountKeys: AccountKeysJson?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves a user auto-unlock key for the given [userId].
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.SharedPreferences
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import com.bitwarden.network.model.AccountKeysJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
@@ -49,7 +48,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 LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -144,7 +142,6 @@ class AuthDiskSourceImpl(
|
||||
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
|
||||
storeEncryptedPin(userId = userId, encryptedPin = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeAccountKeys(userId = userId, accountKeys = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
storeOrganizations(userId = userId, organizations = null)
|
||||
storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
@@ -231,11 +228,9 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use getAccountKeys instead.", replaceWith = ReplaceWith("getAccountKeys"))
|
||||
override fun getPrivateKey(userId: String): String? =
|
||||
getString(key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId))
|
||||
|
||||
@Deprecated("Use storeAccountKeys instead.", replaceWith = ReplaceWith("storeAccountKeys"))
|
||||
override fun storePrivateKey(userId: String, privateKey: String?) {
|
||||
putString(
|
||||
key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId),
|
||||
@@ -243,20 +238,6 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAccountKeys(userId: String): AccountKeysJson? =
|
||||
getEncryptedString(key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId))
|
||||
?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
override fun storeAccountKeys(
|
||||
userId: String,
|
||||
accountKeys: AccountKeysJson?,
|
||||
) {
|
||||
putEncryptedString(
|
||||
key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId),
|
||||
value = accountKeys?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getUserAutoUnlockKey(userId: String): String? =
|
||||
getEncryptedString(
|
||||
key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId),
|
||||
|
||||
@@ -27,12 +27,6 @@ enum class OnboardingStatus {
|
||||
@SerialName("autofillSetup")
|
||||
AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the browser autofill service setup.
|
||||
*/
|
||||
@SerialName("browserAutofillSetup")
|
||||
BROWSER_AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the final step of the onboarding process.
|
||||
*/
|
||||
|
||||
@@ -8,12 +8,12 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
|
||||
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the global state of all users.
|
||||
*/
|
||||
interface UserStateManager {
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
|
||||
*/
|
||||
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an account that is pending deletion in order to allow the account to
|
||||
* remain active until the deletion is finalized.
|
||||
*/
|
||||
var hasPendingAccountDeletion: Boolean
|
||||
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
suspend fun <T> userStateTransaction(block: suspend () -> T): T
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* The default implementation of the [UserStateManager].
|
||||
*/
|
||||
class UserStateManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserStateManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
//region Pending Account Addition
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
get() = mutableHasPendingAccountAdditionStateFlow
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
//endregion Pending Account Addition
|
||||
|
||||
//region Pending Account Deletion
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override var hasPendingAccountDeletion: Boolean
|
||||
by mutableHasPendingAccountDeletionStateFlow::value
|
||||
//endregion Pending Account Deletion
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultLockManager.vaultUnlockDataStateFlow,
|
||||
hasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultLockManager.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultLockManager.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType = authDiskSource
|
||||
.getPinProtectedUserKey(userId = userId)
|
||||
?.let { VaultUnlockType.PIN }
|
||||
?: VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
@@ -26,7 +26,6 @@ fun TrustDeviceResponse.toUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val deviceOptions = decryptionOptions
|
||||
.trustedDeviceUserDecryptionOptions
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
@@ -28,10 +27,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
@@ -44,12 +45,23 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
/**
|
||||
* Models the current auth state.
|
||||
*/
|
||||
val authStateFlow: StateFlow<AuthState>
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
|
||||
*/
|
||||
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
@@ -105,6 +117,15 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
*/
|
||||
var shouldTrustDevice: Boolean
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Return the cached password policies for the current user.
|
||||
*/
|
||||
@@ -126,6 +147,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
*/
|
||||
val showWelcomeCarousel: Boolean
|
||||
|
||||
/**
|
||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||
*/
|
||||
fun clearPendingAccountDeletion()
|
||||
|
||||
/**
|
||||
* Attempt to delete the current account using the [masterPassword] and log them out
|
||||
* upon success.
|
||||
@@ -160,6 +186,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -174,6 +201,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -185,6 +213,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@@ -197,6 +226,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult
|
||||
|
||||
@@ -209,6 +239,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@@ -263,6 +294,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult
|
||||
@@ -300,6 +332,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
passwordHint: String?,
|
||||
): SetPasswordResult
|
||||
|
||||
/**
|
||||
* Set the value of [captchaTokenResultFlow].
|
||||
*/
|
||||
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
|
||||
|
||||
/**
|
||||
* Set the value of [duoTokenResultFlow].
|
||||
*/
|
||||
|
||||
@@ -33,8 +33,6 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
@@ -48,6 +46,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
@@ -56,7 +55,6 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
@@ -79,35 +77,51 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -115,11 +129,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -129,6 +145,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -147,7 +164,6 @@ class AuthRepositoryImpl(
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@@ -157,13 +173,12 @@ class AuthRepositoryImpl(
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val userStateManager: UserStateManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager,
|
||||
UserStateManager by userStateManager {
|
||||
AuthRequestManager by authRequestManager {
|
||||
/**
|
||||
* A scope intended for use when simply collecting multiple flows in order to combine them. The
|
||||
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
|
||||
@@ -176,6 +191,24 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
/**
|
||||
* The auth information to make the identity token request will need to be
|
||||
* cached to make the request again in the case of two-factor authentication.
|
||||
@@ -236,6 +269,72 @@ class AuthRepositoryImpl(
|
||||
initialValue = AuthState.Uninitialized,
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultRepository.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultRepository.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
private val captchaTokenChannel = Channel<CaptchaCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
|
||||
captchaTokenChannel.receiveAsFlow()
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
||||
|
||||
@@ -264,6 +363,9 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||
get() = policyManager.getActivePolicies()
|
||||
|
||||
@@ -282,7 +384,7 @@ class AuthRepositoryImpl(
|
||||
|
||||
init {
|
||||
combine(
|
||||
userStateManager.hasPendingAccountAdditionStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
@@ -301,16 +403,11 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
.onEach { userId ->
|
||||
if (userId == activeUserId) {
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
// We just sync now to get the latest data
|
||||
vaultRepository.sync(forced = true)
|
||||
} else {
|
||||
// We clear the last sync time to ensure we sync when we become the active user
|
||||
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||
}
|
||||
.onEach {
|
||||
val userId = activeUserId ?: return@onEach
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
|
||||
// happens on a background thread
|
||||
@@ -368,12 +465,16 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
override fun clearPendingAccountDeletion() {
|
||||
mutableHasPendingAccountDeletionStateFlow.value = false
|
||||
}
|
||||
|
||||
override suspend fun deleteAccountWithMasterPassword(
|
||||
masterPassword: String,
|
||||
): DeleteAccountResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
return authSdkSource
|
||||
.hashPassword(
|
||||
email = profile.email,
|
||||
@@ -393,7 +494,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun deleteAccountWithOneTimePassword(
|
||||
oneTimePassword: String,
|
||||
): DeleteAccountResult {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
return accountsService
|
||||
.deleteAccount(
|
||||
masterPasswordHash = null,
|
||||
@@ -405,13 +506,13 @@ class AuthRepositoryImpl(
|
||||
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
||||
fold(
|
||||
onFailure = {
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(error = it, message = null)
|
||||
},
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is DeleteAccountResponseJson.Invalid -> {
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(message = response.message, error = null)
|
||||
}
|
||||
|
||||
@@ -462,10 +563,6 @@ class AuthRepositoryImpl(
|
||||
.map { keys }
|
||||
}
|
||||
.onSuccess { keys ->
|
||||
// TDE and SSO user creation still uses crypto-v1. These users are not
|
||||
// expected to have the AEAD keys so we only store the private key for now.
|
||||
// See https://github.com/bitwarden/android/pull/5682#discussion_r2273940332
|
||||
// for more details.
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = keys.privateKey,
|
||||
@@ -494,15 +591,11 @@ class AuthRepositoryImpl(
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
val userId = profile.userId
|
||||
val accountKeys = authDiskSource.getAccountKeys(userId = userId)
|
||||
val privateKey = accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
|
||||
?: authDiskSource.getPrivateKey(userId = userId)
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Private Key"),
|
||||
)
|
||||
val signingKey = accountKeys?.signatureKeyPair?.wrappedSigningKey
|
||||
val securityState = accountKeys?.securityState?.securityState
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
@@ -512,8 +605,6 @@ class AuthRepositoryImpl(
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
@@ -528,6 +619,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult = identityService
|
||||
.preLogin(email = email)
|
||||
.flatMap {
|
||||
@@ -546,6 +638,7 @@ class AuthRepositoryImpl(
|
||||
username = email,
|
||||
password = passwordHash,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
@@ -565,6 +658,7 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult =
|
||||
loginCommon(
|
||||
email = email,
|
||||
@@ -579,12 +673,14 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey = asymmetricalKey,
|
||||
privateKey = requestPrivateKey,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@@ -593,6 +689,7 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
twoFactorData = twoFactorData,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@@ -606,6 +703,7 @@ class AuthRepositoryImpl(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@@ -614,6 +712,7 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@@ -647,6 +746,7 @@ class AuthRepositoryImpl(
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult = loginCommon(
|
||||
email = email,
|
||||
@@ -655,6 +755,7 @@ class AuthRepositoryImpl(
|
||||
ssoCodeVerifier = ssoCodeVerifier,
|
||||
ssoRedirectUri = ssoRedirectUri,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
orgIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
@@ -742,17 +843,7 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationCodeResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -765,17 +856,7 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationOtpResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -798,7 +879,7 @@ class AuthRepositoryImpl(
|
||||
// We need to make sure that the environment is set back to the correct spot.
|
||||
updateEnvironment()
|
||||
// No switching to do but clear any pending account additions
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
hasPendingAccountAddition = false
|
||||
return SwitchAccountResult.NoChange
|
||||
}
|
||||
|
||||
@@ -813,7 +894,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||
|
||||
// Clear any pending account additions
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
hasPendingAccountAddition = false
|
||||
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
@@ -824,6 +905,7 @@ class AuthRepositoryImpl(
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult {
|
||||
@@ -858,6 +940,7 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@@ -874,6 +957,7 @@ class AuthRepositoryImpl(
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
captchaResponse = captchaToken,
|
||||
userSymmetricKey = registerKeyResponse.encryptedUserKey,
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@@ -888,9 +972,18 @@ class AuthRepositoryImpl(
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is RegisterResponseJson.CaptchaRequired -> {
|
||||
it.validationErrors.captchaKeys.firstOrNull()
|
||||
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
|
||||
?: RegisterResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Captcha ID"),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Success -> {
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
RegisterResult.Success
|
||||
RegisterResult.Success(captchaToken = it.captchaBypassToken)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Invalid -> {
|
||||
@@ -1104,9 +1197,6 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.onSuccess {
|
||||
rsaKeys?.private?.let {
|
||||
// This process is used by TDE and Enterprise accounts during initial
|
||||
// login. We continue to store the locally generated keys
|
||||
// until TDE and Enterprise accounts support AEAD keys.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
@@ -1139,6 +1229,10 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||
captchaTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
|
||||
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
|
||||
duoTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
@@ -1476,6 +1570,27 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType =
|
||||
when {
|
||||
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
|
||||
VaultUnlockType.PIN
|
||||
}
|
||||
|
||||
else -> {
|
||||
VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the saved state with the force password reset reason.
|
||||
*/
|
||||
@@ -1509,6 +1624,7 @@ class AuthRepositoryImpl(
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
@@ -1516,6 +1632,7 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
@@ -1534,6 +1651,10 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
when (loginResponse) {
|
||||
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
|
||||
captchaId = loginResponse.captchaKey,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired(
|
||||
loginResponse = loginResponse,
|
||||
email = email,
|
||||
@@ -1586,7 +1707,7 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel?,
|
||||
orgIdentifier: String?,
|
||||
userConfirmedKeyConnector: Boolean,
|
||||
): LoginResult = userStateManager.userStateTransaction {
|
||||
): LoginResult = userStateTransaction {
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
@@ -1625,7 +1746,7 @@ class AuthRepositoryImpl(
|
||||
// we should ask him to confirm the domain
|
||||
if (isNewKeyConnectorUser && isNotConfirmed) {
|
||||
keyConnectorResponse = loginResponse
|
||||
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
|
||||
return LoginResult.ConfirmKeyConnectorDomain(
|
||||
domain = keyConnectorUrl,
|
||||
)
|
||||
}
|
||||
@@ -1681,18 +1802,11 @@ class AuthRepositoryImpl(
|
||||
// when we completed the pending admin auth request.
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||
}
|
||||
// We continue to store the private key for backwards compatibility. Key connector
|
||||
// conversion still relies on the private key.
|
||||
loginResponse.privateKey?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
loginResponse.accountKeys?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storeAccountKeys(userId = userId, accountKeys = it)
|
||||
}
|
||||
// If the user just authenticated with a two-factor code and selected the option to
|
||||
// remember it, then the API response will return a token that will be used in place
|
||||
// of the two-factor code on the next login attempt.
|
||||
@@ -1793,8 +1907,6 @@ class AuthRepositoryImpl(
|
||||
masterKey = it.masterKey,
|
||||
userKey = key,
|
||||
),
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
@@ -1818,8 +1930,6 @@ class AuthRepositoryImpl(
|
||||
val result = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
@@ -1832,16 +1942,10 @@ class AuthRepositoryImpl(
|
||||
userId = profile.userId,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
)
|
||||
// We continue to store the private key for backwards compatibility since
|
||||
// key connector conversion still relies on the private key.
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = profile.userId,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
)
|
||||
authDiskSource.storeAccountKeys(
|
||||
userId = profile.userId,
|
||||
accountKeys = loginResponse.accountKeys,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -1863,13 +1967,11 @@ class AuthRepositoryImpl(
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
val masterPassword = password ?: return null
|
||||
val privateKey = loginResponse.privateKeyOrNull() ?: return null
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = key,
|
||||
@@ -1887,15 +1989,13 @@ class AuthRepositoryImpl(
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
val privateKey = loginResponse.privateKeyOrNull()
|
||||
val privateKey = loginResponse.privateKey
|
||||
val key = loginResponse.key
|
||||
if (privateKey != null && key != null) {
|
||||
deviceData?.let { model ->
|
||||
return unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
@@ -1920,26 +2020,13 @@ class AuthRepositoryImpl(
|
||||
.userDecryptionOptions
|
||||
?.trustedDeviceUserDecryptionOptions
|
||||
?.let { options ->
|
||||
loginResponse.accountKeys
|
||||
?.let { accountKeys ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
|
||||
securityState = accountKeys.securityState?.securityState,
|
||||
signingKey = accountKeys.signatureKeyPair?.wrappedSigningKey,
|
||||
)
|
||||
}
|
||||
?: loginResponse.privateKey
|
||||
?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = null,
|
||||
signingKey = null,
|
||||
)
|
||||
}
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1951,8 +2038,6 @@ class AuthRepositoryImpl(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
profile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
securityState: String?,
|
||||
signingKey: String?,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = profile.userId
|
||||
@@ -1971,8 +2056,6 @@ class AuthRepositoryImpl(
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
@@ -2000,8 +2083,6 @@ class AuthRepositoryImpl(
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = securityState,
|
||||
signingKey = signingKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
@@ -2021,8 +2102,6 @@ class AuthRepositoryImpl(
|
||||
private suspend fun unlockVault(
|
||||
accountProfile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
securityState: String?,
|
||||
signingKey: String?,
|
||||
initUserCryptoMethod: InitUserCryptoMethod,
|
||||
): VaultUnlockResult {
|
||||
val userId = accountProfile.userId
|
||||
@@ -2031,8 +2110,6 @@ class AuthRepositoryImpl(
|
||||
email = accountProfile.email,
|
||||
kdf = accountProfile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = initUserCryptoMethod,
|
||||
// The value for the organization keys here will typically be null. We can separately
|
||||
// unlock the vault for organization data after receiving the sync response if this
|
||||
@@ -2059,12 +2136,20 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
//endregion LoginCommon
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to extract the private key from the
|
||||
* [GetTokenResponseJson.Success] response.
|
||||
*/
|
||||
private fun GetTokenResponseJson.Success.privateKeyOrNull(): String? =
|
||||
this.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
|
||||
?: this.privateKey
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
private inline fun <T> userStateTransaction(block: () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@@ -25,7 +22,6 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -53,7 +49,6 @@ object AuthRepositoryModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
configDiskSource: ConfigDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
@@ -65,8 +60,8 @@ object AuthRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
userStateManager: UserStateManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
@@ -76,7 +71,6 @@ object AuthRepositoryModule {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -89,21 +83,7 @@ object AuthRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesUserStateManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserStateManager = UserStateManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
logsManager = logsManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ sealed class LoginResult {
|
||||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : LoginResult()
|
||||
|
||||
/**
|
||||
* Encryption key migration is required.
|
||||
*/
|
||||
|
||||
@@ -7,8 +7,16 @@ sealed class RegisterResult {
|
||||
/**
|
||||
* Register succeeded.
|
||||
*
|
||||
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
|
||||
*/
|
||||
data object Success : RegisterResult()
|
||||
data class Success(val captchaToken: String?) : RegisterResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*
|
||||
* @param captchaId the captcha id for performing the captcha verification.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : RegisterResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
|
||||
@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
|
||||
*/
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
val error: Throwable,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
|
||||
private const val CAPTCHA_HOST: String = "captcha-callback"
|
||||
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
||||
|
||||
/**
|
||||
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun generateUriForCaptcha(captchaId: String): Uri {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
||||
put(key = "callbackUri", value = CALLBACK_URI)
|
||||
put(key = "captchaRequiredText", value = "Captcha required")
|
||||
}
|
||||
val base64Data = Base64
|
||||
.getEncoder()
|
||||
.encodeToString(
|
||||
json
|
||||
.toString()
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
|
||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=$base64Data&parent=$parentParam&v=1"
|
||||
return Uri.parse(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
|
||||
*
|
||||
* - `null`: Intent is not a captcha callback, or data is null.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.MissingToken]:
|
||||
* Intent is the captcha callback, but its missing a token value.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.Success]:
|
||||
* Intent is the captcha callback, and it has a token.
|
||||
*/
|
||||
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
|
||||
val localData = data
|
||||
return if (
|
||||
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
|
||||
) {
|
||||
localData.getQueryParameter("token")?.let {
|
||||
CaptchaCallbackTokenResult.Success(token = it)
|
||||
} ?: CaptchaCallbackTokenResult.MissingToken
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed class representing the result of captcha callback token extraction.
|
||||
*/
|
||||
sealed class CaptchaCallbackTokenResult {
|
||||
/**
|
||||
* Represents a missing token in the captcha callback.
|
||||
*/
|
||||
data object MissingToken : CaptchaCallbackTokenResult()
|
||||
|
||||
/**
|
||||
* Represents a token present in the captcha callback.
|
||||
*/
|
||||
data class Success(val token: String) : CaptchaCallbackTokenResult()
|
||||
}
|
||||
@@ -32,7 +32,6 @@ fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
@@ -55,23 +54,6 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
val userId = syncProfile.id
|
||||
val account = this.accounts[userId] ?: return this
|
||||
val profile = account.profile
|
||||
val userDecryptionOptions = syncResponse
|
||||
.userDecryption
|
||||
?.let { syncUserDecryption ->
|
||||
profile
|
||||
.userDecryptionOptions
|
||||
?.copy(masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock)
|
||||
?: UserDecryptionOptionsJson(
|
||||
hasMasterPassword = syncUserDecryption.masterPasswordUnlock != null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock,
|
||||
)
|
||||
}
|
||||
?: profile
|
||||
.userDecryptionOptions
|
||||
?.copy(masterPasswordUnlock = null)
|
||||
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
avatarColorHex = syncProfile.avatarColor,
|
||||
@@ -79,7 +61,6 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
@@ -109,7 +90,6 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillCallbackIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.createTotpCopyIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.fillableAutofillIds
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -23,7 +23,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
saveInfo: SaveInfo?,
|
||||
): FillResponse? =
|
||||
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||
Timber.d("Autofill request constructing FillResponse")
|
||||
Timber.w("Autofill request constructing FillResponse")
|
||||
val fillResponseBuilder = FillResponse.Builder()
|
||||
saveInfo?.let { nonNullSaveInfo -> fillResponseBuilder.setSaveInfo(nonNullSaveInfo) }
|
||||
|
||||
@@ -65,8 +65,8 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if the
|
||||
* [FilledPartition.autofillCipher] has a valid cipher id.
|
||||
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if totp is enabled
|
||||
* and there the [FilledPartition.autofillCipher] has a valid cipher id.
|
||||
*/
|
||||
private fun FilledPartition.toAuthIntentSenderOrNull(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
@@ -74,7 +74,8 @@ private fun FilledPartition.toAuthIntentSenderOrNull(
|
||||
autofillCipher
|
||||
.cipherId
|
||||
?.let { cipherId ->
|
||||
createAutofillCallbackIntentSender(
|
||||
// We always do this even if there is no TOTP code because we want to log the events
|
||||
createTotpCopyIntentSender(
|
||||
cipherId = cipherId,
|
||||
context = autofillAppInfo.context,
|
||||
)
|
||||
|
||||
@@ -196,10 +196,6 @@ private fun AutofillCipher.Card.getAutofillValueOrNull(autofillView: AutofillVie
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.Brand -> {
|
||||
brand.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,6 @@ 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.browser.BrowserAutofillDialogManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
@@ -26,8 +24,6 @@ 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.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
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
|
||||
@@ -65,22 +61,6 @@ object AutofillModule {
|
||||
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesBrowserAutofillDialogManager(
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
clock: Clock,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): BrowserAutofillDialogManager = BrowserAutofillDialogManagerImpl(
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
|
||||
clock = clock,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
@@ -128,13 +108,11 @@ object AutofillModule {
|
||||
authRepository: AuthRepository,
|
||||
cipherMatchingManager: CipherMatchingManager,
|
||||
vaultRepository: VaultRepository,
|
||||
policyManager: PolicyManager,
|
||||
): AutofillCipherProvider =
|
||||
AutofillCipherProviderImpl(
|
||||
authRepository = authRepository,
|
||||
cipherMatchingManager = cipherMatchingManager,
|
||||
vaultRepository = vaultRepository,
|
||||
policyManager = policyManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [AutofillCompletionManager].
|
||||
@@ -42,7 +41,6 @@ class AutofillCompletionManagerImpl(
|
||||
.intent
|
||||
?.getAutofillAssistStructureOrNull()
|
||||
?: run {
|
||||
Timber.w("Assist structure not found")
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
@@ -53,7 +51,6 @@ class AutofillCompletionManagerImpl(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
if (autofillRequest !is AutofillRequest.Fillable) {
|
||||
Timber.w("Request is not fillable")
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
@@ -71,13 +68,11 @@ class AutofillCompletionManagerImpl(
|
||||
authIntentSender = null,
|
||||
)
|
||||
?: run {
|
||||
Timber.w("Dataset not found")
|
||||
activity.cancelAndFinish()
|
||||
return@launch
|
||||
}
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
val resultIntent = createAutofillSelectionResultIntent(dataset)
|
||||
Timber.d("Autofill success")
|
||||
activity.setResultAndFinish(resultIntent = resultIntent)
|
||||
cipherView.id?.let {
|
||||
organizationEventManager.trackEvent(
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
/**
|
||||
* Manager to handle whether the Browser Autofill Dialog should be displayed.
|
||||
*/
|
||||
interface BrowserAutofillDialogManager {
|
||||
/**
|
||||
* Number of browsers installed that may need autofill enabled.
|
||||
*/
|
||||
val browserCount: Int
|
||||
|
||||
/**
|
||||
* Indicates whether the dialog should be displayed to the user.
|
||||
*/
|
||||
val shouldShowDialog: Boolean
|
||||
|
||||
/**
|
||||
* The dialog has been dismissed and we should delay displaying it again.
|
||||
*/
|
||||
fun delayDialog()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* We only show the dialog once per 24 hour period.
|
||||
*/
|
||||
private const val SHOW_DIALOG_DELAY_MS: Long = 24L * 60L * 60L * 1000L
|
||||
|
||||
/**
|
||||
* The default implementation of the [BrowserAutofillDialogManager].
|
||||
*/
|
||||
internal class BrowserAutofillDialogManagerImpl(
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
private val clock: Clock,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
) : BrowserAutofillDialogManager {
|
||||
override val browserCount: Int
|
||||
get() = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.availableCount
|
||||
|
||||
override val shouldShowDialog: Boolean
|
||||
get() = autofillEnabledManager.isAutofillEnabled &&
|
||||
browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled &&
|
||||
!firstTimeActionManager
|
||||
.currentOrDefaultUserFirstTimeState
|
||||
.showSetupBrowserAutofillCard &&
|
||||
settingsDiskSource.browserAutofillDialogReshowTime?.isBefore(clock.instant()) != false
|
||||
|
||||
override fun delayDialog() {
|
||||
settingsDiskSource.browserAutofillDialogReshowTime =
|
||||
clock.instant().plusMillis(SHOW_DIALOG_DELAY_MS)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The ID of the cipher associated with this Autofill instance.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillCallbackData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -46,7 +46,6 @@ sealed class AutofillCipher {
|
||||
val expirationMonth: String,
|
||||
val expirationYear: String,
|
||||
val number: String,
|
||||
val brand: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = BitwardenDrawable.ic_payment_card
|
||||
|
||||
@@ -10,7 +10,6 @@ enum class AutofillHint {
|
||||
CARD_EXPIRATION_YEAR,
|
||||
CARD_NUMBER,
|
||||
CARD_SECURITY_CODE,
|
||||
CARD_BRAND,
|
||||
PASSWORD,
|
||||
USERNAME,
|
||||
}
|
||||
|
||||
@@ -16,16 +16,13 @@ sealed class AutofillSaveItem : Parcelable {
|
||||
* @property expirationMonth The expiration month in string form (if applicable).
|
||||
* @property expirationYear The expiration year in string form (if applicable).
|
||||
* @property securityCode The security code for the card (if applicable).
|
||||
* @property cardholderName The name on the card (if applicable).
|
||||
*/
|
||||
@Parcelize
|
||||
data class Card(
|
||||
val cardholderName: String?,
|
||||
val number: String?,
|
||||
val expirationMonth: String?,
|
||||
val expirationYear: String?,
|
||||
val securityCode: String?,
|
||||
val brand: String?,
|
||||
) : AutofillSaveItem()
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for a TOTP copying during the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The cipher for which we are copying a TOTP to the clipboard.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillTotpCopyData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -48,14 +48,10 @@ sealed class AutofillView {
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The expiration year [AutofillView] for the [Card] data partition. This implementation
|
||||
* also has its own [yearValue] because it can be present in lists, in which case there
|
||||
* is specialized logic for determining its [yearValue]. The [Data.textValue] is very
|
||||
* likely going to be a very different value.
|
||||
* The expiration year [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class ExpirationYear(
|
||||
override val data: Data,
|
||||
val yearValue: String?,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
@@ -85,17 +81,6 @@ sealed class AutofillView {
|
||||
data class SecurityCode(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The brand [AutofillView] for the [Card] data partition. This implementation also has its
|
||||
* own [brandValue] because it can be present in lists, in which case there is specialized
|
||||
* logic for determining its [brandValue]. The [Data.textValue] is very likely going to be
|
||||
* a very different value.
|
||||
*/
|
||||
data class Brand(
|
||||
override val data: Data,
|
||||
val brandValue: String?,
|
||||
) : Card()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,9 +6,7 @@ package com.x8bit.bitwarden.data.autofill.model.browser
|
||||
data class BrowserThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
) {
|
||||
val isAvailableButDisabled: Boolean = isAvailable && !isThirdPartyEnabled
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant browsers.
|
||||
@@ -17,20 +15,4 @@ data class BrowserThirdPartyAutofillStatus(
|
||||
val braveStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
|
||||
) {
|
||||
/**
|
||||
* The total number of available browsers.
|
||||
*/
|
||||
val availableCount: Int
|
||||
get() = (if (braveStableStatusData.isAvailable) 1 else 0) +
|
||||
(if (chromeStableStatusData.isAvailable) 1 else 0) +
|
||||
(if (chromeBetaChannelStatusData.isAvailable) 1 else 0)
|
||||
|
||||
/**
|
||||
* Whether any of the available browsers have third party autofill disabled.
|
||||
*/
|
||||
val isAnyIsAvailableAndDisabled: Boolean
|
||||
get() = braveStableStatusData.isAvailableButDisabled ||
|
||||
chromeStableStatusData.isAvailableButDisabled ||
|
||||
chromeBetaChannelStatusData.isAvailableButDisabled
|
||||
}
|
||||
)
|
||||
|
||||
@@ -135,7 +135,7 @@ class AutofillParserImpl(
|
||||
|
||||
// Get inline information if available
|
||||
val isInlineAutofillEnabled = settingsRepository.isInlineAutofillEnabled
|
||||
Timber.d("Autofill request isInlineEnabled=$isInlineAutofillEnabled -- ${fillRequest?.id}")
|
||||
Timber.e("Autofill request isInlineEnabled=$isInlineAutofillEnabled -- ${fillRequest?.id}")
|
||||
val maxInlineSuggestionsCount = fillRequest.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = isInlineAutofillEnabled,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.provider
|
||||
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
@@ -36,7 +34,6 @@ class AutofillCipherProviderImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val cipherMatchingManager: CipherMatchingManager,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val policyManager: PolicyManager,
|
||||
) : AutofillCipherProvider {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
@@ -56,9 +53,7 @@ class AutofillCipherProviderImpl(
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
val organizationIdsWithCardTypeRestrictions = policyManager
|
||||
.getActivePolicies(PolicyTypeJson.RESTRICT_ITEM_TYPES)
|
||||
.map { it.organizationId }
|
||||
|
||||
return cipherListViews
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView
|
||||
@@ -69,11 +64,7 @@ class AutofillCipherProviderImpl(
|
||||
// Must not be deleted.
|
||||
it.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE &&
|
||||
// Must not be restricted by organization.
|
||||
!it.isExcludedByOrgCardRestrictions(
|
||||
organizationIdsWithCardTypeRestrictions,
|
||||
)
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
?.let { nonNullCipherListView ->
|
||||
nonNullCipherListView.id?.let { cipherId ->
|
||||
@@ -87,7 +78,6 @@ class AutofillCipherProviderImpl(
|
||||
expirationMonth = cipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = cipherView.card?.expYear.orEmpty(),
|
||||
number = cipherView.card?.number.orEmpty(),
|
||||
brand = cipherView.card?.brand.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,33 +138,10 @@ class AutofillCipherProviderImpl(
|
||||
Timber.e("Cipher not found for autofill.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(result.error, "Failed to decrypt cipher for autofill.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [CipherListView] item should be excluded from autofill due to
|
||||
* organization-based card type restrictions.
|
||||
*
|
||||
* It's considered restricted if:
|
||||
* 1. There are organizations with card type restrictions AND this item is a personal vault item
|
||||
* (organizationId is null).
|
||||
* 2. OR this item belongs to an organization that has card type restrictions.
|
||||
*/
|
||||
private fun CipherListView.isExcludedByOrgCardRestrictions(
|
||||
restrictingOrgIds: List<String>,
|
||||
): Boolean {
|
||||
if (restrictingOrgIds.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
// If personal vault (no orgId), restricted if any org has restrictions.
|
||||
return organizationId == null ||
|
||||
// If part of an org, restricted if that org is in the restricting list.
|
||||
organizationId in restrictingOrgIds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,18 @@ import android.service.autofill.Dataset
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.core.os.bundleOf
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
|
||||
import com.x8bit.bitwarden.AutofillCallbackActivity
|
||||
import com.x8bit.bitwarden.AutofillTotpCopyActivity
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCallbackData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data"
|
||||
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
|
||||
private const val AUTOFILL_CALLBACK_DATA_KEY = "autofill-callback-data"
|
||||
private const val AUTOFILL_TOTP_COPY_DATA_KEY = "autofill-totp-copy-data"
|
||||
private const val AUTOFILL_BUNDLE_KEY = "autofill-bundle-key"
|
||||
|
||||
/**
|
||||
@@ -55,21 +54,21 @@ fun createAutofillSelectionIntent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [IntentSender] built with the data required for performing an Autofill callback
|
||||
* during the autofill flow.
|
||||
* Creates an [IntentSender] built with the data required for performing a TOTP copying during
|
||||
* the autofill flow.
|
||||
*/
|
||||
fun createAutofillCallbackIntentSender(
|
||||
fun createTotpCopyIntentSender(
|
||||
cipherId: String,
|
||||
context: Context,
|
||||
): IntentSender {
|
||||
val intent = Intent(
|
||||
context,
|
||||
AutofillCallbackActivity::class.java,
|
||||
AutofillTotpCopyActivity::class.java,
|
||||
)
|
||||
.putExtra(
|
||||
AUTOFILL_BUNDLE_KEY,
|
||||
bundleOf(
|
||||
AUTOFILL_CALLBACK_DATA_KEY to AutofillCallbackData(cipherId = cipherId),
|
||||
AUTOFILL_TOTP_COPY_DATA_KEY to AutofillTotpCopyData(cipherId = cipherId),
|
||||
),
|
||||
)
|
||||
return PendingIntent
|
||||
@@ -143,12 +142,12 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
?.getSafeParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains Autofill callback data. The [AutofillCallbackData] will be
|
||||
* Checks if the given [Intent] contains data for TOTP copying. The [AutofillTotpCopyData] will be
|
||||
* returned when present.
|
||||
*/
|
||||
fun Intent.getAutofillCallbackIntentOrNull(): AutofillCallbackData? =
|
||||
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
|
||||
getBundleExtra(AUTOFILL_BUNDLE_KEY)
|
||||
?.getSafeParcelableExtra(AUTOFILL_CALLBACK_DATA_KEY)
|
||||
?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the
|
||||
|
||||
@@ -9,19 +9,16 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
val AutofillPartition.Card.expirationMonthSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.ExpirationMonth>()
|
||||
.firstOrNull { it.monthValue != null }
|
||||
?.monthValue
|
||||
.firstOrNull { it is AutofillView.Card.ExpirationMonth && it.monthValue != null }
|
||||
?.data
|
||||
?.textValue
|
||||
|
||||
/**
|
||||
* The text value representation of the year from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.expirationYearSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.ExpirationYear>()
|
||||
.firstOrNull { it.yearValue != null }
|
||||
?.yearValue
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.ExpirationYear }
|
||||
|
||||
/**
|
||||
* The text value representation of the card number from the [AutofillPartition.Card].
|
||||
@@ -37,24 +34,6 @@ val AutofillPartition.Card.securityCodeSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.SecurityCode }
|
||||
|
||||
/**
|
||||
* The text value representation of the cardholder name from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.cardholderNameSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.CardholderName }
|
||||
|
||||
/**
|
||||
* The text value representation of the brand from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.brandSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.Brand>()
|
||||
.firstOrNull { it.brandValue != null }
|
||||
?.brandValue
|
||||
?: this.extractNonNullTextValueOrNull { it is AutofillView.Card.Brand }
|
||||
|
||||
/**
|
||||
* The text value representation of the password from the [AutofillPartition.Login].
|
||||
*/
|
||||
|
||||
@@ -11,12 +11,10 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
when (this.partition) {
|
||||
is AutofillPartition.Card -> {
|
||||
AutofillSaveItem.Card(
|
||||
cardholderName = partition.cardholderNameSaveValue,
|
||||
number = partition.numberSaveValue,
|
||||
expirationMonth = partition.expirationMonthSaveValue,
|
||||
expirationYear = partition.expirationYearSaveValue,
|
||||
securityCode = partition.securityCodeSaveValue,
|
||||
brand = partition.brandSaveValue,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,39 +35,3 @@ fun AutofillValue.extractTextValue(): String? =
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a year value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractYearValue(
|
||||
autofillOptions: List<String>,
|
||||
): String? =
|
||||
when {
|
||||
this.isList && autofillOptions.isNotEmpty() -> {
|
||||
autofillOptions.getOrNull(listValue)
|
||||
}
|
||||
|
||||
this.isText -> {
|
||||
this.textValue.toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a card brand value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractCardBrandValue(
|
||||
autofillOptions: List<String>,
|
||||
): String? =
|
||||
when {
|
||||
this.isList && autofillOptions.isNotEmpty() -> {
|
||||
autofillOptions.getOrNull(listValue)
|
||||
}
|
||||
|
||||
this.isText -> {
|
||||
this.textValue.toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.view.View
|
||||
import android.view.autofill.AutofillValue
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
|
||||
/**
|
||||
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
|
||||
@@ -13,34 +11,29 @@ import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
fun AutofillView.buildFilledItemOrNull(
|
||||
value: String,
|
||||
): FilledItem? =
|
||||
// Do not try to autofill fields that are empty in the vault
|
||||
if (value.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (this.data.autofillType) {
|
||||
View.AUTOFILL_TYPE_DATE -> {
|
||||
value
|
||||
.toLongOrNull()
|
||||
?.let { AutofillValue.forDate(it) }
|
||||
}
|
||||
|
||||
View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value)
|
||||
View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value)
|
||||
View.AUTOFILL_TYPE_TOGGLE -> {
|
||||
value
|
||||
.toBooleanStrictOrNull()
|
||||
?.let { AutofillValue.forToggle(it) }
|
||||
}
|
||||
|
||||
else -> null
|
||||
when (this.data.autofillType) {
|
||||
View.AUTOFILL_TYPE_DATE -> {
|
||||
value
|
||||
.toLongOrNull()
|
||||
?.let { AutofillValue.forDate(it) }
|
||||
}
|
||||
?.let { autofillValue ->
|
||||
FilledItem(
|
||||
autofillId = this.data.autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value)
|
||||
View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value)
|
||||
View.AUTOFILL_TYPE_TOGGLE -> {
|
||||
value
|
||||
.toBooleanStrictOrNull()
|
||||
?.let { AutofillValue.forToggle(it) }
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
?.let { autofillValue ->
|
||||
FilledItem(
|
||||
autofillId = this.data.autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list [AutofillValue] out of [value] or return null if not possible.
|
||||
@@ -49,50 +42,22 @@ fun AutofillView.buildFilledItemOrNull(
|
||||
private fun AutofillView.buildListAutofillValueOrNull(
|
||||
value: String,
|
||||
): AutofillValue? =
|
||||
when (this) {
|
||||
is AutofillView.Card.ExpirationMonth -> {
|
||||
val autofillOptionsSize = this.data.autofillOptions.size
|
||||
// The idea here is that `value` is a numerical representation of a month.
|
||||
val monthIndex = value.toIntOrNull()
|
||||
when {
|
||||
monthIndex == null -> null
|
||||
// We expect there is some placeholder or empty space at the beginning of the list.
|
||||
autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex)
|
||||
autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationYear -> {
|
||||
val autofillOptions = this.data.autofillOptions
|
||||
autofillOptions
|
||||
.firstOrNull { it == value || it.takeLast(2) == value.takeLast(2) }
|
||||
?.let { AutofillValue.forList(autofillOptions.indexOf(it)) }
|
||||
}
|
||||
|
||||
is AutofillView.Card.Brand -> {
|
||||
value.findVaultCardBrandWithNameOrNull()
|
||||
?.takeUnless { it == VaultCardBrand.SELECT }
|
||||
?.let { vaultCardBrand ->
|
||||
this.data.autofillOptions
|
||||
.firstOrNull { it.findVaultCardBrandWithNameOrNull() == vaultCardBrand }
|
||||
?.let { AutofillValue.forList(this.data.autofillOptions.indexOf(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.CardholderName,
|
||||
is AutofillView.Card.ExpirationDate,
|
||||
is AutofillView.Card.Number,
|
||||
is AutofillView.Card.SecurityCode,
|
||||
is AutofillView.Login.Password,
|
||||
is AutofillView.Login.Username,
|
||||
is AutofillView.Unused,
|
||||
-> {
|
||||
this
|
||||
.data
|
||||
.autofillOptions
|
||||
.indexOfFirst { it == value }
|
||||
.takeIf { it != -1 }
|
||||
?.let { AutofillValue.forList(it) }
|
||||
if (this is AutofillView.Card.ExpirationMonth) {
|
||||
val autofillOptionsSize = this.data.autofillOptions.size
|
||||
// The idea here is that `value` is a numerical representation of a month.
|
||||
val monthIndex = value.toIntOrNull()
|
||||
when {
|
||||
monthIndex == null -> null
|
||||
// We expect there is some placeholder or empty space at the beginning of the list.
|
||||
autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex)
|
||||
autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
this
|
||||
.data
|
||||
.autofillOptions
|
||||
.indexOfFirst { it == value }
|
||||
.takeIf { it != -1 }
|
||||
?.let { AutofillValue.forList(it) }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
expirationMonth = card.expMonth.orEmpty(),
|
||||
expirationYear = card.expYear.orEmpty(),
|
||||
number = card.number.orEmpty(),
|
||||
brand = card.brand.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
|
||||
@@ -52,12 +52,6 @@ fun HtmlInfo?.isCardExpirationDateField(): Boolean = isInputField &&
|
||||
fun HtmlInfo?.isCardSecurityCodeField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card brand field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardBrandField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS)
|
||||
|
||||
/**
|
||||
* Attributes that can be used as hints to determine the type of data the associated node expects.
|
||||
*
|
||||
@@ -103,11 +97,7 @@ private fun List<String>.containsAnyPatterns(patterns: List<Regex>): Boolean = t
|
||||
* Checks if the list of strings contains any of the specified terms.
|
||||
*/
|
||||
private fun List<String>.containsAnyTerms(terms: List<String>): Boolean =
|
||||
this.any { string ->
|
||||
string
|
||||
.toLowerCaseAndStripNonAlpha()
|
||||
.containsAnyTerms(terms)
|
||||
}
|
||||
this.any { string -> string.containsAnyTerms(terms) }
|
||||
|
||||
/**
|
||||
* The supported attribute keys whose value can represent an autofill hint.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [Int] is a password [InputType].
|
||||
@@ -29,3 +32,15 @@ val Int.isUsernameInputType: Boolean
|
||||
* Whether this [Int] contains [flag].
|
||||
*/
|
||||
private fun Int.hasFlag(flag: Int): Boolean = (this and flag) == flag
|
||||
|
||||
/**
|
||||
* Starting from an initial pending intent flag. (ex: [PendingIntent.FLAG_CANCEL_CURRENT])
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun Int.toPendingIntentMutabilityFlag(): Int =
|
||||
// Mutable flag was added on API level 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
this or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -26,10 +26,3 @@ fun String.matchesAnyExpressions(
|
||||
expressions.any {
|
||||
this.matches(regex = it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this [String] to lowercase and remove all non-alpha characters.
|
||||
*/
|
||||
fun String.toLowerCaseAndStripNonAlpha(): String = this
|
||||
.lowercase()
|
||||
.replace(Regex("[^a-z]"), "")
|
||||
|
||||
@@ -92,7 +92,6 @@ private val AssistStructure.ViewNode.supportedAutofillHint: AutofillHint?
|
||||
this.isCardNumberField -> AutofillHint.CARD_NUMBER
|
||||
this.isCardSecurityCodeField -> AutofillHint.CARD_SECURITY_CODE
|
||||
this.isCardholderNameField -> AutofillHint.CARD_CARDHOLDER
|
||||
this.isCardBrandField -> AutofillHint.CARD_BRAND
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -123,7 +122,6 @@ private fun String.toBitwardenAutofillHintOrNull(): AutofillHint? =
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillOptions: List<String>,
|
||||
autofillViewData: AutofillView.Data,
|
||||
@@ -143,15 +141,8 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
}
|
||||
|
||||
AutofillHint.CARD_EXPIRATION_YEAR -> {
|
||||
val yearValue = this
|
||||
.autofillValue
|
||||
?.extractYearValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
yearValue = yearValue,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -191,18 +182,7 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_BRAND -> {
|
||||
val brandValue = this.autofillValue
|
||||
?.extractCardBrandValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
AutofillView.Card.Brand(
|
||||
data = autofillViewData,
|
||||
brandValue = brandValue,
|
||||
)
|
||||
}
|
||||
|
||||
null -> {
|
||||
else -> {
|
||||
AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
@@ -250,8 +230,7 @@ internal val AssistStructure.ViewNode.isUsernameField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration month field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationMonthField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardExpirationMonthField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationMonthField()
|
||||
@@ -259,8 +238,7 @@ internal val AssistStructure.ViewNode.isCardExpirationMonthField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration year field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationYearField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardExpirationYearField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationYearField()
|
||||
@@ -268,8 +246,7 @@ internal val AssistStructure.ViewNode.isCardExpirationYearField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration date field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationDateField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardExpirationDateField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationDateField()
|
||||
@@ -277,8 +254,7 @@ internal val AssistStructure.ViewNode.isCardExpirationDateField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card number field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardNumberField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardNumberField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardNumberField()
|
||||
@@ -286,8 +262,7 @@ internal val AssistStructure.ViewNode.isCardNumberField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card security code field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardSecurityCodeField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardSecurityCodeField: Boolean
|
||||
get() =
|
||||
idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
@@ -296,25 +271,11 @@ internal val AssistStructure.ViewNode.isCardSecurityCodeField: Boolean
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a cardholder name field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardholderNameField: Boolean
|
||||
private val AssistStructure.ViewNode.isCardholderNameField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardholderNameField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card brand field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardBrandField: Boolean
|
||||
get() = idEntry
|
||||
?.toLowerCaseAndStripNonAlpha()
|
||||
?.containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS) == true ||
|
||||
hint
|
||||
?.toLowerCaseAndStripNonAlpha()
|
||||
?.containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS) == true ||
|
||||
htmlInfo.isCardBrandField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] contains any ignored hint terms.
|
||||
*/
|
||||
@@ -322,7 +283,6 @@ private fun AssistStructure.ViewNode.containsIgnoredHintTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.htmlInfo.hints().any { it.containsAnyTerms(IGNORED_RAW_HINTS) }
|
||||
|
||||
/**
|
||||
* The website that this [AssistStructure.ViewNode] is a part of representing.
|
||||
*/
|
||||
|
||||
@@ -135,15 +135,3 @@ val SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:cc|card)(?:[\\s_-](?:verification|security))?([\\s_-]code)\\b"
|
||||
.toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported card brand autofill hints.
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_BRAND_HINTS: List<String> = listOf(
|
||||
"cctype",
|
||||
"creditcardtype",
|
||||
"cardtype",
|
||||
"cardbrand",
|
||||
"creditcardbrand",
|
||||
"ccbrand",
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryImpl
|
||||
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.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -92,11 +93,13 @@ object CredentialProviderModule {
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
privilegedAppRepository: PrivilegedAppRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): OriginManager =
|
||||
OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
privilegedAppRepository = privilegedAppRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
@@ -16,7 +15,6 @@ import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
@@ -26,6 +24,7 @@ import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithCopyablePassword
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
@@ -42,6 +41,7 @@ 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.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.fold
|
||||
@@ -207,6 +207,8 @@ class BitwardenCredentialManagerImpl(
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.toPublicKeyCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherListViews = cipherListViews
|
||||
.filter { it.isActiveWithFido2Credentials },
|
||||
)
|
||||
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
|
||||
|
||||
@@ -223,72 +225,65 @@ class BitwardenCredentialManagerImpl(
|
||||
|
||||
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): Result<List<CredentialEntry>> {
|
||||
if (this.isEmpty()) return emptyList<CredentialEntry>().asSuccess()
|
||||
val assertionOptions = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson) }
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException(
|
||||
"Passkey assertion options required.",
|
||||
)
|
||||
.asFailure()
|
||||
}
|
||||
|
||||
val relyingPartyIds = assertionOptions
|
||||
.mapNotNull { it.relyingPartyId }
|
||||
.toSet()
|
||||
val relyingPartyIds = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
|
||||
.distinct()
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException("Relying party id required.").asFailure()
|
||||
}
|
||||
|
||||
val allowedCredentials = assertionOptions
|
||||
.flatMap { option ->
|
||||
option
|
||||
.allowCredentials
|
||||
?.map { it.id }
|
||||
val cipherViews = cipherListViews
|
||||
.filter { cipherListView ->
|
||||
cipherListView.login
|
||||
?.fido2Credentials
|
||||
.orEmpty()
|
||||
.any { credential -> credential.rpId in relyingPartyIds }
|
||||
}
|
||||
.mapNotNull { cipherListView ->
|
||||
when (val result = vaultRepository.getCipher(cipherListView.id.orEmpty())) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found while building public key credential entries.")
|
||||
null
|
||||
}
|
||||
|
||||
val discoveredCredentials = relyingPartyIds
|
||||
.flatMap { relyingPartyId ->
|
||||
vaultSdkSource
|
||||
.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to discover credentials.")
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(
|
||||
result.error,
|
||||
"Failed to decrypt cipher while building credential entries.",
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
}
|
||||
.filterAllowedCredentialsIfNecessary(allowedCredentials)
|
||||
.toTypedArray()
|
||||
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
|
||||
|
||||
return credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = discoveredCredentials,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
cipherViews = cipherViews,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { fido2AutofillViews ->
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.asSuccess()
|
||||
},
|
||||
onFailure = {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
},
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.filterAllowedCredentialsIfNecessary(
|
||||
allowedCredentialIds: List<String>,
|
||||
): List<Fido2CredentialAutofillView> = if (allowedCredentialIds.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
this.filter {
|
||||
Base64
|
||||
.encodeToString(
|
||||
it.credentialId,
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
|
||||
) in allowedCredentialIds
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
|
||||
import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import timber.log.Timber
|
||||
@@ -21,6 +23,7 @@ class OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val privilegedAppRepository: PrivilegedAppRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
@@ -67,7 +70,10 @@ class OriginManagerImpl(
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
.takeUnless {
|
||||
it is ValidateOriginResult.Error.PrivilegedAppNotAllowed &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.UserManagedPrivilegedApps)
|
||||
}
|
||||
?: validatePrivilegedAppSignatureWithUserTrustList(callingAppInfo)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
|
||||
@@ -20,7 +20,7 @@ data class Fido2AttestationResponse(
|
||||
@SerialName("response")
|
||||
val response: RegistrationResponse,
|
||||
@SerialName("clientExtensionResults")
|
||||
val clientExtensionResults: ClientExtensionResults,
|
||||
val clientExtensionResults: ClientExtensionResults?,
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: String?,
|
||||
) {
|
||||
@@ -50,7 +50,7 @@ data class Fido2AttestationResponse(
|
||||
@Serializable
|
||||
data class ClientExtensionResults(
|
||||
@SerialName("credProps")
|
||||
val credentialProperties: CredentialProperties? = null,
|
||||
val credentialProperties: CredentialProperties,
|
||||
) {
|
||||
/**
|
||||
* Represents properties for newly created credential.
|
||||
|
||||
@@ -105,11 +105,6 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
|
||||
|
||||
/**
|
||||
* The time at which the browser autofill dialog is allowed to be shown to the user again.
|
||||
*/
|
||||
var browserAutofillDialogReshowTime: Instant?
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
@@ -286,23 +281,6 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
fun getUserHasSignedInPreviously(userId: String): Boolean
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
*/
|
||||
fun getShowBrowserAutofillSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* enable the browser autofill integration in onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowAutoFillSettingBadge] for the given [userId].
|
||||
*/
|
||||
fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
|
||||
@@ -37,7 +37,6 @@ private const val CLEAR_CLIPBOARD_INTERVAL_KEY = "clearClipboard"
|
||||
private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
|
||||
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
|
||||
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
|
||||
private const val SHOW_BROWSER_AUTOFILL_SETTING_BADGE = "showBrowserAutofillSettingBadge"
|
||||
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
|
||||
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
|
||||
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
|
||||
@@ -49,7 +48,6 @@ private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMa
|
||||
private const val RESUME_SCREEN = "resumeScreen"
|
||||
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
|
||||
private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
|
||||
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -74,9 +72,6 @@ class SettingsDiskSourceImpl(
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowBrowserAutofillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -229,12 +224,6 @@ class SettingsDiskSourceImpl(
|
||||
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
|
||||
get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) }
|
||||
|
||||
override var browserAutofillDialogReshowTime: Instant?
|
||||
get() = getLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME)?.let { Instant.ofEpochMilli(it) }
|
||||
set(value) {
|
||||
putLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME, value = value?.toEpochMilli())
|
||||
}
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
@@ -442,21 +431,6 @@ class SettingsDiskSourceImpl(
|
||||
key = HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY.appendIdentifier(userId),
|
||||
) == true
|
||||
|
||||
override fun getShowBrowserAutofillSettingBadge(userId: String): Boolean? =
|
||||
getBoolean(key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId))
|
||||
|
||||
override fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
|
||||
value = showBadge,
|
||||
)
|
||||
getMutableShowBrowserAutofillSettingBadgeFlow(userId).tryEmit(showBadge)
|
||||
}
|
||||
|
||||
override fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShowBrowserAutofillSettingBadgeFlow(userId = userId)
|
||||
.onSubscription { emit(getShowBrowserAutofillSettingBadge(userId)) }
|
||||
|
||||
override fun getShowAutoFillSettingBadge(userId: String): Boolean? =
|
||||
getBoolean(
|
||||
key = SHOW_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
|
||||
@@ -624,13 +598,6 @@ class SettingsDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowBrowserAutofillSettingBadgeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutableShowBrowserAutofillSettingBadgeFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowAutoFillSettingBadgeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.di
|
||||
|
||||
import com.bitwarden.core.data.manager.BuildInfoManager
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.bitwardenServiceClient
|
||||
import com.bitwarden.network.interceptor.BaseUrlsProvider
|
||||
@@ -14,6 +13,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CL
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.util.isDevBuild
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -54,7 +54,6 @@ object PlatformNetworkModule {
|
||||
baseUrlsProvider: BaseUrlsProvider,
|
||||
authDiskSource: AuthDiskSource,
|
||||
certificateManager: CertificateManager,
|
||||
buildInfoManager: BuildInfoManager,
|
||||
clock: Clock,
|
||||
): BitwardenServiceClient = bitwardenServiceClient(
|
||||
BitwardenServiceClientConfig(
|
||||
@@ -68,7 +67,7 @@ object PlatformNetworkModule {
|
||||
authTokenProvider = authTokenManager,
|
||||
baseUrlsProvider = baseUrlsProvider,
|
||||
certificateProvider = certificateManager,
|
||||
enableHttpBodyLogging = buildInfoManager.isDevBuild,
|
||||
enableHttpBodyLogging = isDevBuild,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that the security stamps for the current user do not match.
|
||||
*/
|
||||
class SecurityStampMismatchException : IllegalStateException("Security stamps do not match!")
|
||||
@@ -63,12 +63,6 @@ interface FirstTimeActionManager {
|
||||
*/
|
||||
fun storeShowUnlockSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable the browser autofill integration later, during onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable autofill later, during onboarding.
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
@@ -26,14 +25,12 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Implementation of [FirstTimeActionManager]
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FirstTimeActionManagerImpl @Inject constructor(
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) : FirstTimeActionManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
@@ -81,12 +78,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
combine(
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = it),
|
||||
) { showAutofillBadge, showBrowserAutofillBadge ->
|
||||
listOf(showAutofillBadge, showBrowserAutofillBadge)
|
||||
}
|
||||
// Can be expanded to support multiple autofill settings
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOfNotNull(showAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
@@ -128,7 +124,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
),
|
||||
) {
|
||||
FirstTimeState(
|
||||
@@ -136,11 +131,19 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupUnlockCard = it[1],
|
||||
showSetupAutofillCard = it[2],
|
||||
showImportLoginsCardInSettings = it[3],
|
||||
showSetupBrowserAutofillCard = it[4],
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart { emit(currentOrDefaultUserFirstTimeState) }
|
||||
.onStart {
|
||||
emit(
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
@@ -173,11 +176,14 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
|
||||
showImportLoginsCardInSettings = settingsDiskSource
|
||||
.getShowImportLoginsSettingBadge(it),
|
||||
showSetupBrowserAutofillCard = settingsDiskSource
|
||||
.getShowBrowserAutofillSettingBadge(it),
|
||||
)
|
||||
}
|
||||
?: FirstTimeState()
|
||||
?: FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
)
|
||||
|
||||
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
@@ -187,14 +193,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storeShowBrowserAutofillSettingBadge(
|
||||
userId = activeUserId,
|
||||
showBadge = showBadge,
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeShowAutoFillSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storeShowAutoFillSettingBadge(
|
||||
@@ -259,19 +257,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showBrowserAutofill value which takes
|
||||
* into account if autofill and if browser autofill is already enabled.
|
||||
*/
|
||||
private fun getShowBrowserAutofillSettingBadgeFlowInternal(userId: String): Flow<Boolean> =
|
||||
combine(
|
||||
settingsDiskSource.getShowBrowserAutofillSettingBadgeFlow(userId = userId),
|
||||
autofillEnabledManager.isAutofillEnabledStateFlow,
|
||||
thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatusFlow,
|
||||
) { showBadge, autofillEnabled, status ->
|
||||
showBadge ?: false && autofillEnabled && status.isAnyIsAvailableAndDisabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showAutofill value which takes
|
||||
* into account if autofill is already enabled globally.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bitwarden.data.manager
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Manager for loading native libraries.
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bitwarden.data.manager
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import timber.log.Timber
|
||||
@@ -7,7 +7,7 @@ import timber.log.Timber
|
||||
* Primary implementation of [NativeLibraryManager].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
internal class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> {
|
||||
return try {
|
||||
System.loadLibrary(libraryName)
|
||||
@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended for full syncs for the user ID provided.
|
||||
* Flow that represents requests intended for full syncs.
|
||||
*/
|
||||
val fullSyncFlow: Flow<String>
|
||||
val fullSyncFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to log a user out.
|
||||
@@ -52,7 +52,7 @@ interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended to trigger syncing organization keys.
|
||||
*/
|
||||
val syncOrgKeysFlow: Flow<String>
|
||||
val syncOrgKeysFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync send delete.
|
||||
|
||||
@@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
@@ -55,7 +54,7 @@ class PushManagerImpl @Inject constructor(
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
@@ -67,13 +66,13 @@ class PushManagerImpl @Inject constructor(
|
||||
bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncSendDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendDeleteData>()
|
||||
private val mutableSyncSendUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
|
||||
override val fullSyncFlow: SharedFlow<String>
|
||||
override val fullSyncFlow: SharedFlow<Unit>
|
||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||
|
||||
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
||||
@@ -94,7 +93,7 @@ class PushManagerImpl @Inject constructor(
|
||||
override val syncFolderUpsertFlow: SharedFlow<SyncFolderUpsertData>
|
||||
get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncOrgKeysFlow: SharedFlow<String>
|
||||
override val syncOrgKeysFlow: SharedFlow<Unit>
|
||||
get() = mutableSyncOrgKeysSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncSendDeleteFlow: SharedFlow<SyncSendDeleteData>
|
||||
@@ -130,8 +129,8 @@ class PushManagerImpl @Inject constructor(
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun onMessageReceived(notification: BitwardenNotification) {
|
||||
if (authDiskSource.uniqueAppId == notification.contextId) return
|
||||
|
||||
val userId = activeUserId ?: return
|
||||
Timber.d("Push Notification Received: ${notification.notificationType}")
|
||||
|
||||
when (val type = notification.notificationType) {
|
||||
NotificationType.AUTH_REQUEST,
|
||||
@@ -190,24 +189,16 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.cipherId != null }
|
||||
?.let {
|
||||
SyncCipherDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.cipherId
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(SyncCipherDeleteData(it)) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHERS,
|
||||
NotificationType.SYNC_SETTINGS,
|
||||
NotificationType.SYNC_VAULT,
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
|
||||
.userId
|
||||
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
|
||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
NotificationType.SYNC_FOLDER_CREATE,
|
||||
@@ -235,24 +226,15 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.folderId != null }
|
||||
?.let {
|
||||
SyncFolderDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
folderId = requireNotNull(it.folderId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.folderId
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_ORG_KEYS -> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SynchronizeOrganizationKeysNotifications>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.userId
|
||||
.takeIf { authDiskSource.userState?.accounts.orEmpty().containsKey(it) }
|
||||
?.let { mutableSyncOrgKeysSharedFlow.tryEmit(it) }
|
||||
if (isLoggedIn(userId)) {
|
||||
mutableSyncOrgKeysSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType.SYNC_SEND_CREATE,
|
||||
@@ -280,14 +262,9 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.sendId != null }
|
||||
?.let {
|
||||
SyncSendDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
sendId = requireNotNull(it.sendId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.sendId
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(SyncSendDeleteData(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* The token provider to pass to the SDK.
|
||||
*/
|
||||
class Token : ClientManagedTokens {
|
||||
override suspend fun getAccessToken(): String? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
@@ -24,7 +13,7 @@ class SdkClientManagerImpl(
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
|
||||
Client(tokenProvider = Token(), settings = null).apply {
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
|
||||
@@ -3,13 +3,10 @@ package com.x8bit.bitwarden.data.platform.manager.di
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.bitwarden.core.data.manager.realtime.RealtimeManager
|
||||
import com.bitwarden.core.data.manager.realtime.RealtimeManagerImpl
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManagerImpl
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.EventService
|
||||
@@ -19,7 +16,6 @@ import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@@ -43,6 +39,8 @@ import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -200,10 +198,6 @@ object PlatformManagerModule {
|
||||
toastManager = toastManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRealtimeManager(): RealtimeManager = RealtimeManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideToastManager(
|
||||
@@ -242,6 +236,10 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
@@ -331,8 +329,13 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun provideRestrictionManager(
|
||||
@ApplicationContext context: Context,
|
||||
appStateManager: AppStateManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
): RestrictionManager = RestrictionManagerImpl(
|
||||
appStateManager = appStateManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
context = context,
|
||||
environmentRepository = environmentRepository,
|
||||
restrictionsManager = requireNotNull(context.getSystemService()),
|
||||
)
|
||||
@@ -351,14 +354,12 @@ object PlatformManagerModule {
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
): FirstTimeActionManager = FirstTimeActionManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
thirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -190,7 +190,7 @@ internal class FlightRecorderManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FlightRecorderTree : Timber.DebugTree() {
|
||||
private inner class FlightRecorderTree : Timber.Tree() {
|
||||
var flightRecorderData: FlightRecorderDataSet.FlightRecorderData? = null
|
||||
set(value) {
|
||||
value?.let {
|
||||
|
||||
@@ -8,7 +8,6 @@ data class FirstTimeState(
|
||||
val showImportLoginsCardInSettings: Boolean,
|
||||
val showSetupUnlockCard: Boolean,
|
||||
val showSetupAutofillCard: Boolean,
|
||||
val showSetupBrowserAutofillCard: Boolean,
|
||||
) {
|
||||
/**
|
||||
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
|
||||
@@ -19,12 +18,10 @@ data class FirstTimeState(
|
||||
showSetupUnlockCard: Boolean? = null,
|
||||
showSetupAutofillCard: Boolean? = null,
|
||||
showImportLoginsCardInSettings: Boolean? = null,
|
||||
showSetupBrowserAutofillCard: Boolean? = null,
|
||||
) : this(
|
||||
showImportLoginsCard = showImportLoginsCard ?: true,
|
||||
showSetupUnlockCard = showSetupUnlockCard ?: false,
|
||||
showSetupAutofillCard = showSetupAutofillCard ?: false,
|
||||
showImportLoginsCardInSettings = showImportLoginsCardInSettings ?: false,
|
||||
showSetupBrowserAutofillCard = showSetupBrowserAutofillCard ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,21 +74,4 @@ sealed class NotificationPayload {
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
* A notification payload for resynchronizing organization keys.
|
||||
*/
|
||||
@Serializable
|
||||
data class SynchronizeOrganizationKeysNotifications(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
* A notification payload for syncing a users vault.
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncNotification(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
) : NotificationPayload()
|
||||
}
|
||||
|
||||
@@ -105,13 +105,4 @@ sealed class OrganizationEvent {
|
||||
override val type: OrganizationEventType
|
||||
get() = OrganizationEventType.CIPHER_CLIENT_VIEWED
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks when user's individual vault is exported.
|
||||
*/
|
||||
data object UserClientExportedVault : OrganizationEvent() {
|
||||
override val cipherId: String? = null
|
||||
override val type: OrganizationEventType
|
||||
get() = OrganizationEventType.USER_CLIENT_EXPORTED_VAULT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CredentialManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@@ -134,14 +133,6 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object VerificationCodeShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched to select an account to export credentials from.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CredentialExchangeExport(
|
||||
val data: ImportCredentialsRequestData,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync cipher delete operations.
|
||||
*
|
||||
* @property cipherId The cipher ID.
|
||||
*/
|
||||
data class SyncCipherDeleteData(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync folder delete operations.
|
||||
*
|
||||
* @property folderId The folder ID.
|
||||
*/
|
||||
data class SyncFolderDeleteData(
|
||||
val userId: String,
|
||||
val folderId: String,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync send delete operations.
|
||||
*
|
||||
* @property sendId The send ID.
|
||||
*/
|
||||
data class SyncSendDeleteData(
|
||||
val userId: String,
|
||||
val sendId: String,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,4 @@ package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
/**
|
||||
* A manager for handling restrictions.
|
||||
*/
|
||||
interface RestrictionManager {
|
||||
/**
|
||||
* Initializes the [RestrictionManager].
|
||||
*/
|
||||
fun initialize()
|
||||
}
|
||||
interface RestrictionManager
|
||||
|
||||
@@ -1,19 +1,57 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.RestrictionsManager
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import android.os.Bundle
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* The default implementation of the [RestrictionManager].
|
||||
*/
|
||||
class RestrictionManagerImpl(
|
||||
appStateManager: AppStateManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val context: Context,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val restrictionsManager: RestrictionsManager,
|
||||
) : RestrictionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
private val intentFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
|
||||
private val restrictionsChangedReceiver = RestrictionsChangedReceiver()
|
||||
private var isReceiverRegistered = false
|
||||
|
||||
override fun initialize() {
|
||||
init {
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
when (it) {
|
||||
AppForegroundState.BACKGROUNDED -> handleBackground()
|
||||
AppForegroundState.FOREGROUNDED -> handleForeground()
|
||||
}
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
private fun handleBackground() {
|
||||
if (isReceiverRegistered) {
|
||||
context.unregisterReceiver(restrictionsChangedReceiver)
|
||||
}
|
||||
isReceiverRegistered = false
|
||||
}
|
||||
|
||||
private fun handleForeground() {
|
||||
context.registerReceiver(restrictionsChangedReceiver, intentFilter)
|
||||
isReceiverRegistered = true
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
|
||||
@@ -21,22 +59,65 @@ class RestrictionManagerImpl(
|
||||
restrictionsManager
|
||||
.applicationRestrictions
|
||||
?.takeUnless { it.isEmpty }
|
||||
?.getString(BASE_ENVIRONMENT_URL_RESTRICTION_KEY)
|
||||
?.let { setPreconfiguredSettings(it) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredSettings(bundle: Bundle) {
|
||||
bundle
|
||||
.getString(BASE_ENVIRONMENT_URL_RESTRICTION_KEY)
|
||||
?.let { url -> setPreconfiguredUrl(baseEnvironmentUrl = url) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredUrl(baseEnvironmentUrl: String) {
|
||||
environmentRepository.environment = when (baseEnvironmentUrl) {
|
||||
// If the baseEnvironmentUrl matches the predefined US environment, assume it is the
|
||||
// default US environment.
|
||||
Environment.Us.environmentUrlData.base -> Environment.Us
|
||||
// If the baseEnvironmentUrl matches the predefined EU environment, assume it is the
|
||||
// default EU environment.
|
||||
Environment.Eu.environmentUrlData.base -> Environment.Eu
|
||||
// Otherwise make a custom self-host environment.
|
||||
else -> Environment.SelfHosted(EnvironmentUrlDataJson(baseEnvironmentUrl))
|
||||
environmentRepository.environment = when (val current = environmentRepository.environment) {
|
||||
Environment.Us -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined US environment, leave it alone
|
||||
Environment.Us.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined US environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Environment.Eu -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined EU environment, leave it alone
|
||||
Environment.Eu.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined EU environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
is Environment.SelfHosted -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] used to listen for [Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED]
|
||||
* updates.
|
||||
*
|
||||
* Note: The `Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED` will only be received if the
|
||||
* `BroadcastReceiver` is dynamically registered, so this cannot be registered in the manifest.
|
||||
*/
|
||||
private inner class RestrictionsChangedReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) {
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val BASE_ENVIRONMENT_URL_RESTRICTION_KEY: String = "baseEnvironmentUrl"
|
||||
|
||||
/**
|
||||
* Helper method for creating a new [Environment.SelfHosted] with a new base.
|
||||
*/
|
||||
private fun Environment.toSelfHosted(
|
||||
base: String,
|
||||
): Environment.SelfHosted =
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = environmentUrlData.copy(base = base),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
@@ -72,12 +71,3 @@ fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
|
||||
is SpecialCircumstance.AddTotpLoginItem -> this.data
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ImportCredentialsRequestData] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toImportCredentialsRequestDataOrNull(): ImportCredentialsRequestData? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.CredentialExchangeExport -> this.data
|
||||
else -> null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user