Compare commits

..

5 Commits

Author SHA1 Message Date
Patrick Honkonen
5523d99400 [PM-14346] Run alias generation on the IO dispatcher (#4215)
(cherry picked from commit 8f2d55c146)
2024-11-01 12:04:28 -04:00
David Perez
9f8d21cb95 PM-14255: Remove accessibility logic to improve overall performance (#4206)
(cherry picked from commit 4831750ffd)
2024-11-01 12:04:28 -04:00
Patrick Honkonen
75fc9fe210 [PM-14254] Keep Android verifier for JNI usage (#4197)
(cherry picked from commit fab018782c)
2024-11-01 12:04:27 -04:00
Álison Fernandes
42671aadfb [PM-14224] Automate Play Store prod variant publishing (#4183) 2024-10-30 14:13:04 +00:00
aj-rosado
d71389ab02 [PM-14241] Checking if Timber tree has been added before trying to remove it (#4194) 2024-10-30 13:12:20 +01:00
509 changed files with 5188 additions and 14834 deletions

View File

@@ -1,4 +1,4 @@
name: Android Bug Report
name: Android Beta Bug Report
description: File a bug report
labels: [ bug ]
body:
@@ -7,7 +7,19 @@ body:
value: |
Thanks for taking the time to fill out this bug report!
> [!WARNING]
> This is the new native Bitwarden Beta app repository. For the publicly available apps in App Store / Play Store, submit your report in [bitwarden/mobile](https://github.com/bitwarden/mobile)
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: checkboxes
id: beta
attributes:
label: Bitwarden Beta
options:
- label: "I'm using the new native Bitwarden Beta app and I'm aware that legacy .NET app bugs should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)"
validations:
required: true
- type: textarea
id: reproduce
attributes:
@@ -51,22 +63,6 @@ body:
description: What version of our software are you running?
validations:
required: true
- type: dropdown
id: server-region
attributes:
label: What server are you connecting to?
options:
- US
- EU
- Self-host
- N/A
validations:
required: true
- type: input
id: server-version
attributes:
label: Self-host Server Version
description: If self-hosting, what version of Bitwarden Server are you running?
- type: textarea
id: environment-details
attributes:

View File

@@ -37,13 +37,13 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -53,7 +53,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -62,13 +62,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -103,10 +103,10 @@ jobs:
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Ruby
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -160,7 +160,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -170,7 +170,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -179,7 +179,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -393,10 +393,10 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Ruby
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -436,7 +436,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -446,7 +446,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -455,7 +455,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -464,17 +464,10 @@ jobs:
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionName:${{ inputs.version-name || '' }}
regex='versionName = "([^"]+)"'
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
@@ -535,11 +528,11 @@ jobs:
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |

View File

@@ -14,7 +14,7 @@ jobs:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -29,7 +29,7 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -14,7 +14,7 @@ jobs:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
@@ -29,7 +29,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -1,127 +0,0 @@
name: Create GitHub Release
on:
workflow_dispatch:
inputs:
version-name:
description: 'Version Name - E.g. "2024.11.1"'
required: true
type: string
version-number:
description: 'Version Number - E.g. "123456"'
required: true
type: string
artifact_run_id:
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
draft:
description: 'Create as draft release'
type: boolean
default: true
prerelease:
description: 'Mark as pre-release'
type: boolean
make_latest:
description: 'Set as the latest release'
type: boolean
branch-protection-type:
description: 'Branch protection type'
type: choice
options:
- Branch Name
- GitHub API
default: Branch Name
env:
ARTIFACTS_PATH: artifacts
jobs:
create-release:
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read
steps:
- name: Check out repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- name: Get branch from workflow run
id: get_release_branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
run: |
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
case "$BRANCH_PROTECTION_TYPE" in
"Branch Name")
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
;;
"GitHub API")
#NOTE requires token with "administration:read" scope
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
exit 1
fi
;;
*)
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
exit 1
;;
esac
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
- name: Download artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
run: |
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
echo "Downloaded $file_count file(s)."
if [ "$file_count" -gt 0 ]; then
echo "Downloaded files:"
find $ARTIFACTS_PATH -type f
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
with:
tag_name: ${{ inputs.version-name }}
name: "v${{ inputs.version-name }} (${{ inputs.version-number }})"
prerelease: ${{ inputs.prerelease }}
draft: ${{ inputs.draft }}
make_latest: ${{ inputs.make_latest }}
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
generate_release_notes: true
files: |
artifacts/**/*
- name: Update Release Description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
RELEASE_URL: ${{ steps.create_release.outputs.url }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
run: |
# Get current release body
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
# Append build source to the end
updated_body="${current_body}
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
# Update release
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$updated_body"
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,56 +0,0 @@
name: Cut Release Branch
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Type'
required: true
type: choice
options:
- RC
- Hotfix
rc_prefix_date:
description: 'RC - Prefix with date. E.g. 2024.11-rc1'
type: boolean
default: true
jobs:
create-release-branch:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- name: Create RC Branch
if: inputs.release_type == 'RC'
env:
RC_PREFIX_DATE: ${{ inputs.rc_prefix_date }}
run: |
if [ "$RC_PREFIX_DATE" = "true" ]; then
current_date=$(date +'%Y.%m')
branch_name="release/${current_date}-rc${{ github.run_number }}"
else
branch_name="release/rc${{ github.run_number }}"
fi
git switch main
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
- name: Create Hotfix Branch
if: inputs.release_type == 'Hotfix'
run: |
latest_tag=$(git describe --tags --abbrev=0)
if [ -z "$latest_tag" ]; then
echo "::error::No tags found in the repository"
exit 1
fi
branch_name="release/hotfix-${latest_tag}"
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY

View File

@@ -28,12 +28,12 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -48,7 +48,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with:
sarif_file: cx_result.sarif
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -41,7 +41,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -51,7 +51,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -60,12 +60,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1003.0)
aws-sdk-core (3.212.0)
aws-partitions (1.989.0)
aws-sdk-core (3.209.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.170.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -32,7 +32,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
date (3.4.0)
date (3.3.4)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
@@ -69,7 +69,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.225.0)
fastlane (2.224.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -85,7 +85,6 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@@ -114,8 +113,6 @@ GEM
fastlane-plugin-firebase_app_distribution (0.9.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -162,17 +159,17 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.8.1)
json (2.7.2)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
@@ -182,7 +179,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.9)
rexml (3.3.8)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -195,11 +192,10 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
time (0.4.1)
time (0.4.0)
date
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
@@ -209,12 +205,12 @@ GEM
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)

View File

@@ -1,4 +1,7 @@
# Bitwarden Android
# Bitwarden Android (BETA)
> [!TIP]
> This repo has the new native Android app, currently in [Beta](https://community.bitwarden.com/t/about-the-beta-program/39185). Looking for the legacy .NET MAUI apps? Head on over to [bitwarden/mobile](https://github.com/bitwarden/mobile)
## Contents
@@ -9,7 +12,7 @@
## Compatibility
- **Minimum SDK**: 29
- **Target SDK**: 35
- **Target SDK**: 34
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape

View File

@@ -135,10 +135,7 @@ android {
unitTests.isReturnDefaultValues = true
}
lint {
disable += listOf(
"MissingTranslation",
"ExtraTranslation",
)
disable.add("MissingTranslation")
}
}
@@ -162,7 +159,8 @@ dependencies {
add("standardImplementation", dependencyNotation)
}
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
// TODO: this should use a versioned AAR instead of referencing a local AAR BITAU-94
implementation(files("libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)

View File

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

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_generator_shortcut"
android:shortcutId="bitwarden_password_generator"
android:shortcutLongLabel="@string/password_generator"
android:shortcutShortLabel="@string/password_generator">
<intent
android:action="android.intent.action.VIEW"
android:data="bitwarden://password_generator"
android:targetClass="com.x8bit.bitwarden.MainActivity"
android:targetPackage="com.x8bit.bitwarden.beta" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_vault_shortcut"
android:shortcutId="bitwarden_my_vault"
android:shortcutLongLabel="@string/my_vault"
android:shortcutShortLabel="@string/my_vault">
<intent
android:action="android.intent.action.VIEW"
android:data="bitwarden://my_vault"
android:targetClass="com.x8bit.bitwarden.MainActivity"
android:targetPackage="com.x8bit.bitwarden.beta" />
</shortcut>
</shortcuts>

View File

@@ -190,14 +190,12 @@ class MainViewModel @Inject constructor(
private fun handleAccessibilitySelectionReceive(
action: MainAction.Internal.AccessibilitySelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
}
private fun handleAutofillSelectionReceive(
action: MainAction.Internal.AutofillSelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
}

View File

@@ -181,11 +181,6 @@ interface AuthDiskSource {
*/
fun storeUserBiometricUnlockKey(userId: String, biometricsKey: String?)
/**
* Gets the flow for the biometrics key for the given [userId].
*/
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a pin-protected user key for the given [userId].
*/
@@ -203,11 +198,6 @@ interface AuthDiskSource {
inMemoryOnly: Boolean = false,
)
/**
* Retrieves a flow for the pin-protected user key for the given [userId].
*/
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
/**
* Gets a two-factor auth token using a user's [email].
*/

View File

@@ -74,10 +74,6 @@ class AuthDiskSourceImpl(
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableBiometricUnlockKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -288,13 +284,8 @@ class AuthDiskSourceImpl(
key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId),
value = biometricsKey,
)
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
}
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
getMutableBiometricUnlockKeyFlow(userId)
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
@@ -310,13 +301,8 @@ class AuthDiskSourceImpl(
key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId),
value = pinProtectedUserKey,
)
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
override fun getTwoFactorToken(email: String): String? =
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
@@ -520,18 +506,6 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableBiometricUnlockKeyFlow(
userId: String,
): MutableSharedFlow<String?> = mutableBiometricUnlockKeyFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePinProtectedUserKeyFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -2,10 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Represents the current account information for a given user.
@@ -47,7 +45,6 @@ data class AccountJson(
* @property kdfParallelism The number of threads to use when calculating a password hash.
* @property userDecryptionOptions The options available to a user for decryption.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Profile(
@SerialName("userId")
@@ -89,8 +86,7 @@ data class AccountJson(
@SerialName("kdfParallelism")
val kdfParallelism: Int?,
@SerialName("userDecryptionOptions")
@JsonNames("accountDecryptionOptions")
@SerialName("accountDecryptionOptions")
val userDecryptionOptions: UserDecryptionOptionsJson?,
)

View File

@@ -7,21 +7,13 @@ import kotlinx.serialization.Serializable
* Container for the user's API tokens.
*
* @property requestId The ID of the pending Auth Request.
* @property requestPrivateKey The private key of the pending Auth Request.
* @property requestAccessCode The access code of the pending Auth Request.
* @property requestFingerprint The fingerprint of the pending Auth Request.
* @property requestPrivateKey The private of the pending Auth Request.
*/
@Serializable
data class PendingAuthRequestJson(
@SerialName("id")
@SerialName("Id")
val requestId: String,
@SerialName("privateKey")
@SerialName("PrivateKey")
val requestPrivateKey: String,
@SerialName("accessCode")
val requestAccessCode: String,
@SerialName("fingerprint")
val requestFingerprint: String,
)

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountReque
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.HTTP
import retrofit2.http.POST
@@ -19,43 +18,43 @@ interface AuthenticatedAccountsApi {
* Converts the currently active account to a key-connector account.
*/
@POST("/accounts/convert-to-key-connector")
suspend fun convertToKeyConnector(): NetworkResult<Unit>
suspend fun convertToKeyConnector(): Result<Unit>
/**
* Creates the keys for the current account.
*/
@POST("/accounts/keys")
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result<Unit>
/**
* Deletes the current account.
*/
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult<Unit>
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
@POST("/accounts/request-otp")
suspend fun requestOtp(): NetworkResult<Unit>
suspend fun requestOtp(): Result<Unit>
@POST("/accounts/verify-otp")
suspend fun verifyOtp(
@Body body: VerifyOtpRequestJson,
): NetworkResult<Unit>
): Result<Unit>
/**
* Resets the temporary password.
*/
@HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true)
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
/**
* Resets the password.
*/
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
/**
* Sets the password.
*/
@POST("/accounts/set-password")
suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult<Unit>
suspend fun setPassword(@Body body: SetPasswordRequestJson): Result<Unit>
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -23,7 +22,7 @@ interface AuthenticatedAuthRequestsApi {
suspend fun createAdminAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson,
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
): Result<AuthRequestsResponseJson.AuthRequest>
/**
* Updates an authentication request.
@@ -32,13 +31,13 @@ interface AuthenticatedAuthRequestsApi {
suspend fun updateAuthRequest(
@Path("id") userId: String,
@Body body: AuthRequestUpdateRequestJson,
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
): Result<AuthRequestsResponseJson.AuthRequest>
/**
* Gets a list of auth requests for this device.
*/
@GET("/auth-requests")
suspend fun getAuthRequests(): NetworkResult<AuthRequestsResponseJson>
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
/**
* Retrieves an existing authentication request by ID.
@@ -46,5 +45,5 @@ interface AuthenticatedAuthRequestsApi {
@GET("/auth-requests/{requestId}")
suspend fun getAuthRequest(
@Path("requestId") requestId: String,
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
): Result<AuthRequestsResponseJson.AuthRequest>
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.PUT
import retrofit2.http.Path
@@ -17,5 +16,5 @@ interface AuthenticatedDevicesApi {
suspend fun updateTrustedDeviceKeys(
@Path(value = "appId") appId: String,
@Body request: TrustedDeviceKeysRequestJson,
): NetworkResult<TrustedDeviceKeysResponseJson>
): Result<TrustedDeviceKeysResponseJson>
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
@@ -16,5 +15,5 @@ interface AuthenticatedKeyConnectorApi {
suspend fun storeMasterKeyToKeyConnector(
@Url url: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): NetworkResult<Unit>
): Result<Unit>
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
@@ -21,7 +20,7 @@ interface AuthenticatedOrganizationApi {
@Path("orgId") organizationId: String,
@Path("userId") userId: String,
@Body body: OrganizationResetPasswordEnrollRequestJson,
): NetworkResult<Unit>
): Result<Unit>
/**
* Checks whether this organization auto enrolls users in password reset.
@@ -29,7 +28,7 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{identifier}/auto-enroll-status")
suspend fun getOrganizationAutoEnrollResponse(
@Path("identifier") organizationIdentifier: String,
): NetworkResult<OrganizationAutoEnrollStatusResponseJson>
): Result<OrganizationAutoEnrollStatusResponseJson>
/**
* Gets the public and private keys for this organization.
@@ -37,5 +36,5 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{id}/keys")
suspend fun getOrganizationKeys(
@Path("id") organizationId: String,
): NetworkResult<OrganizationKeysResponseJson>
): Result<OrganizationKeysResponseJson>
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
@@ -15,5 +14,5 @@ interface HaveIBeenPwnedApi {
suspend fun fetchBreachedPasswords(
@Path("hashPrefix")
hashPrefix: String,
): NetworkResult<ResponseBody>
): Result<ResponseBody>
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.Header
@@ -16,16 +15,16 @@ interface UnauthenticatedAccountsApi {
@POST("/accounts/password-hint")
suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson,
): NetworkResult<Unit>
): Result<Unit>
@POST("/two-factor/send-email-login")
suspend fun resendVerificationCodeEmail(
@Body body: ResendEmailRequestJson,
): NetworkResult<Unit>
): Result<Unit>
@POST("/accounts/set-key-connector-key")
suspend fun setKeyConnectorKey(
@Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): NetworkResult<Unit>
): Result<Unit>
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -22,7 +21,7 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun createAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson,
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
): Result<AuthRequestsResponseJson.AuthRequest>
/**
* Queries for updates to a given auth request.
@@ -31,5 +30,5 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun getAuthRequestUpdate(
@Path("requestId") requestId: String,
@Query("code") accessCode: String,
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
): Result<AuthRequestsResponseJson.AuthRequest>
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET
import retrofit2.http.Header
@@ -12,5 +11,5 @@ interface UnauthenticatedDevicesApi {
suspend fun getIsKnownDevice(
@Header(value = "X-Request-Email") emailAddress: String,
@Header(value = "X-Device-Identifier") deviceId: String,
): NetworkResult<Boolean>
): Result<Boolean>
}

View File

@@ -10,7 +10,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import kotlinx.serialization.json.JsonPrimitive
import retrofit2.Call
import retrofit2.http.Body
@@ -47,12 +46,12 @@ interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?,
): NetworkResult<GetTokenResponseJson.Success>
): Result<GetTokenResponseJson.Success>
@GET("/sso/prevalidate")
suspend fun prevalidateSso(
@Query("domainHint") organizationIdentifier: String,
): NetworkResult<PrevalidateSsoResponseJson>
): Result<PrevalidateSsoResponseJson>
/**
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
@@ -67,25 +66,23 @@ interface UnauthenticatedIdentityApi {
): Call<RefreshTokenResponseJson>
@POST("/accounts/prelogin")
suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult<PreLoginResponseJson>
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
@POST("/accounts/register")
suspend fun register(
@Body body: RegisterRequestJson,
): NetworkResult<RegisterResponseJson.Success>
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
@POST("/accounts/register/finish")
suspend fun registerFinish(
@Body body: RegisterFinishRequestJson,
): NetworkResult<RegisterResponseJson.Success>
): Result<RegisterResponseJson.Success>
@POST("/accounts/register/send-verification-email")
suspend fun sendVerificationEmail(
@Body body: SendVerificationEmailRequestJson,
): NetworkResult<JsonPrimitive?>
): Result<JsonPrimitive?>
@POST("/accounts/register/verification-email-clicked")
suspend fun verifyEmailToken(
@Body body: VerifyEmailTokenRequestJson,
): NetworkResult<Unit>
): Result<Unit>
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.GET
@@ -21,11 +20,11 @@ interface UnauthenticatedKeyConnectorApi {
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): NetworkResult<Unit>
): Result<Unit>
@GET
suspend fun getMasterKeyFromKeyConnector(
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): NetworkResult<KeyConnectorMasterKeyResponseJson>
): Result<KeyConnectorMasterKeyResponseJson>
}

View File

@@ -2,9 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.POST
@@ -18,13 +15,5 @@ interface UnauthenticatedOrganizationApi {
@POST("/organizations/domain/sso/details")
suspend fun getClaimedDomainOrganizationDetails(
@Body body: OrganizationDomainSsoDetailsRequestJson,
): NetworkResult<OrganizationDomainSsoDetailsResponseJson>
/**
* Checks for the verfied organization domains of an email for SSO purposes.
*/
@POST("/organizations/domain/sso/verified")
suspend fun getVerifiedOrganizationDomainsByEmail(
@Body body: VerifiedOrganizationDomainSsoDetailsRequest,
): NetworkResult<VerifiedOrganizationDomainSsoDetailsResponse>
): Result<OrganizationDomainSsoDetailsResponseJson>
}

View File

@@ -1,19 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's key connector.
*
* @property keyConnectorUrl URL to the user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class KeyConnectorUserDecryptionOptionsJson(
@SerialName("keyConnectorUrl")
@JsonNames("KeyConnectorUrl")
@SerialName("KeyConnectorUrl")
val keyConnectorUrl: String,
)

View File

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

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's trusted device.
@@ -15,26 +13,20 @@ import kotlinx.serialization.json.JsonNames
* @property hasManageResetPasswordPermission Whether or not the user has manage reset password
* permission.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class TrustedDeviceUserDecryptionOptionsJson(
@SerialName("encryptedPrivateKey")
@JsonNames("EncryptedPrivateKey")
@SerialName("EncryptedPrivateKey")
val encryptedPrivateKey: String?,
@SerialName("encryptedUserKey")
@JsonNames("EncryptedUserKey")
@SerialName("EncryptedUserKey")
val encryptedUserKey: String?,
@SerialName("hasAdminApproval")
@JsonNames("HasAdminApproval")
@SerialName("HasAdminApproval")
val hasAdminApproval: Boolean,
@SerialName("hasLoginApprovingDevice")
@JsonNames("HasLoginApprovingDevice")
@SerialName("HasLoginApprovingDevice")
val hasLoginApprovingDevice: Boolean,
@SerialName("hasManageResetPasswordPermission")
@JsonNames("HasManageResetPasswordPermission")
@SerialName("HasManageResetPasswordPermission")
val hasManageResetPasswordPermission: Boolean,
)

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* The options available to a user for decryption.
@@ -14,18 +12,14 @@ import kotlinx.serialization.json.JsonNames
* device.
* @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class UserDecryptionOptionsJson(
@SerialName("hasMasterPassword")
@JsonNames("HasMasterPassword")
@SerialName("HasMasterPassword")
val hasMasterPassword: Boolean,
@SerialName("trustedDeviceOption")
@JsonNames("TrustedDeviceOption")
@SerialName("TrustedDeviceOption")
val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?,
@SerialName("keyConnectorOption")
@JsonNames("KeyConnectorOption")
@SerialName("KeyConnectorOption")
val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?,
)

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body object when retrieving organization verified domain SSO info.
*
* @param email The email address to check against.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsRequest(
@SerialName("email") val email: String,
)

View File

@@ -1,35 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Response object returned when requesting organization verified domain SSO details.
*
* @property verifiedOrganizationDomainSsoDetails The list of verified organization domain SSO
* details.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsResponse(
@SerialName("data")
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) {
/**
* Response body for an organization verified domain SSO details.
*
* @property organizationName The name of the organization.
* @property organizationIdentifier The identifier of the organization.
* @property domainName The name of the domain.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetail(
@SerialName("organizationName")
val organizationName: String,
@SerialName("organizationIdentifier")
val organizationIdentifier: String,
@SerialName("domainName")
val domainName: String,
)
}

View File

@@ -19,7 +19,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJs
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import kotlinx.serialization.json.Json
/**
@@ -38,22 +37,18 @@ class AccountsServiceImpl(
* Converts the currently active account to a key-connector account.
*/
override suspend fun convertToKeyConnector(): Result<Unit> =
authenticatedAccountsApi
.convertToKeyConnector()
.toResult()
authenticatedAccountsApi.convertToKeyConnector()
override suspend fun createAccountKeys(
publicKey: String,
encryptedPrivateKey: String,
): Result<Unit> =
authenticatedAccountsApi
.createAccountKeys(
body = CreateAccountKeysRequest(
publicKey = publicKey,
encryptedPrivateKey = encryptedPrivateKey,
),
)
.toResult()
authenticatedAccountsApi.createAccountKeys(
body = CreateAccountKeysRequest(
publicKey = publicKey,
encryptedPrivateKey = encryptedPrivateKey,
),
)
override suspend fun deleteAccount(
masterPasswordHash: String?,
@@ -66,8 +61,9 @@ class AccountsServiceImpl(
oneTimePassword = oneTimePassword,
),
)
.toResult()
.map { DeleteAccountResponseJson.Success }
.map {
DeleteAccountResponseJson.Success
}
.recoverCatching { throwable ->
throwable
.toBitwardenError()
@@ -79,25 +75,20 @@ class AccountsServiceImpl(
}
override suspend fun requestOneTimePasscode(): Result<Unit> =
authenticatedAccountsApi
.requestOtp()
.toResult()
authenticatedAccountsApi.requestOtp()
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
authenticatedAccountsApi
.verifyOtp(
VerifyOtpRequestJson(
oneTimePasscode = passcode,
),
)
.toResult()
authenticatedAccountsApi.verifyOtp(
VerifyOtpRequestJson(
oneTimePasscode = passcode,
),
)
override suspend fun requestPasswordHint(
email: String,
): Result<PasswordHintResponseJson> =
unauthenticatedAccountsApi
.passwordHintRequest(PasswordHintRequestJson(email))
.toResult()
.map { PasswordHintResponseJson.Success }
.recoverCatching { throwable ->
throwable
@@ -110,70 +101,54 @@ class AccountsServiceImpl(
}
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
unauthenticatedAccountsApi
.resendVerificationCodeEmail(body = body)
.toResult()
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
if (body.currentPasswordHash == null) {
authenticatedAccountsApi
.resetTempPassword(body = body)
.toResult()
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
return if (body.currentPasswordHash == null) {
authenticatedAccountsApi.resetTempPassword(body = body)
} else {
authenticatedAccountsApi
.resetPassword(body = body)
.toResult()
authenticatedAccountsApi.resetPassword(body = body)
}
}
override suspend fun setKeyConnectorKey(
accessToken: String,
body: KeyConnectorKeyRequestJson,
): Result<Unit> =
unauthenticatedAccountsApi
.setKeyConnectorKey(
body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
override suspend fun setPassword(
body: SetPasswordRequestJson,
): Result<Unit> = authenticatedAccountsApi
.setPassword(body)
.toResult()
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
override suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson> =
unauthenticatedKeyConnectorApi
.getMasterKeyFromKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
override suspend fun storeMasterKeyToKeyConnector(
url: String,
masterKey: String,
): Result<Unit> =
authenticatedKeyConnectorApi
.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
override suspend fun storeMasterKeyToKeyConnector(
url: String,
accessToken: String,
masterKey: String,
): Result<Unit> =
unauthenticatedKeyConnectorApi
.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
}

View File

@@ -3,22 +3,17 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class AuthRequestsServiceImpl(
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
) : AuthRequestsService {
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
authenticatedAuthRequestsApi
.getAuthRequests()
.toResult()
authenticatedAuthRequestsApi.getAuthRequests()
override suspend fun getAuthRequest(
requestId: String,
): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId)
override suspend fun updateAuthRequest(
requestId: String,
@@ -27,15 +22,13 @@ class AuthRequestsServiceImpl(
deviceId: String,
isApproved: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi
.updateAuthRequest(
userId = requestId,
body = AuthRequestUpdateRequestJson(
key = key,
masterPasswordHash = masterPasswordHash,
deviceId = deviceId,
isApproved = isApproved,
),
)
.toResult()
authenticatedAuthRequestsApi.updateAuthRequest(
userId = requestId,
body = AuthRequestUpdateRequestJson(
key = key,
masterPasswordHash = masterPasswordHash,
deviceId = deviceId,
isApproved = isApproved,
),
)
}

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevic
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class DevicesServiceImpl(
private val authenticatedDevicesApi: AuthenticatedDevicesApi,
@@ -14,26 +13,22 @@ class DevicesServiceImpl(
override suspend fun getIsKnownDevice(
emailAddress: String,
deviceId: String,
): Result<Boolean> = unauthenticatedDevicesApi
.getIsKnownDevice(
emailAddress = emailAddress.base64UrlEncode(),
deviceId = deviceId,
)
.toResult()
): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice(
emailAddress = emailAddress.base64UrlEncode(),
deviceId = deviceId,
)
override suspend fun trustDevice(
appId: String,
encryptedUserKey: String,
encryptedDevicePublicKey: String,
encryptedDevicePrivateKey: String,
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi
.updateTrustedDeviceKeys(
appId = appId,
request = TrustedDeviceKeysRequestJson(
encryptedUserKey = encryptedUserKey,
encryptedDevicePublicKey = encryptedDevicePublicKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
),
)
.toResult()
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys(
appId = appId,
request = TrustedDeviceKeysRequestJson(
encryptedUserKey = encryptedUserKey,
encryptedDevicePublicKey = encryptedDevicePublicKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
),
)
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import java.security.MessageDigest
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
@@ -18,7 +17,6 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
return api
.fetchBreachedPasswords(hashPrefix = hashPrefix)
.toResult()
.mapCatching { responseBody ->
responseBody.string()
// First split the response by newline: each hashed password is on a new line.

View File

@@ -16,9 +16,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRe
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
import kotlinx.serialization.json.Json
@@ -29,15 +28,12 @@ class IdentityServiceImpl(
) : IdentityService {
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
unauthenticatedIdentityApi
.preLogin(PreLoginRequestJson(email = email))
.toResult()
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
@Suppress("MagicNumber")
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
.register(body)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
@@ -79,7 +75,6 @@ class IdentityServiceImpl(
captchaResponse = captchaToken,
authRequestId = authModel.authRequestId,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
@@ -100,7 +95,6 @@ class IdentityServiceImpl(
.prevalidateSso(
organizationIdentifier = organizationIdentifier,
)
.toResult()
override fun refreshTokenSynchronously(
refreshToken: String,
@@ -110,8 +104,7 @@ class IdentityServiceImpl(
grantType = "refresh_token",
refreshToken = refreshToken,
)
.executeForNetworkResult()
.toResult()
.executeForResult()
@Suppress("MagicNumber")
override suspend fun registerFinish(
@@ -119,7 +112,6 @@ class IdentityServiceImpl(
): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
.registerFinish(body)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
@@ -135,7 +127,6 @@ class IdentityServiceImpl(
): Result<String?> {
return unauthenticatedIdentityApi
.sendVerificationEmail(body = body)
.toResult()
.map { it?.content }
}
@@ -145,8 +136,9 @@ class IdentityServiceImpl(
.verifyEmailToken(
body = body,
)
.toResult()
.map { VerifyEmailTokenResponseJson.Valid }
.map {
VerifyEmailTokenResponseJson.Valid
}
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthR
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.asFailure
/**
@@ -25,19 +24,17 @@ class NewAuthRequestServiceImpl(
): Result<AuthRequestsResponseJson.AuthRequest> =
when (authRequestType) {
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
unauthenticatedAuthRequestsApi
.createAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
.toResult()
unauthenticatedAuthRequestsApi.createAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
}
AuthRequestTypeJson.UNLOCK -> {
@@ -46,19 +43,17 @@ class NewAuthRequestServiceImpl(
}
AuthRequestTypeJson.ADMIN_APPROVAL -> {
authenticatedAuthRequestsApi
.createAdminAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
.toResult()
authenticatedAuthRequestsApi.createAdminAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
}
}
@@ -68,15 +63,11 @@ class NewAuthRequestServiceImpl(
isSso: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> =
if (isSso) {
authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
authenticatedAuthRequestsApi.getAuthRequest(requestId)
} else {
unauthenticatedAuthRequestsApi
.getAuthRequestUpdate(
requestId = requestId,
accessCode = accessCode,
)
.toResult()
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
requestId = requestId,
accessCode = accessCode,
)
}
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
/**
* Provides an API for querying organization endpoints.
@@ -39,12 +38,4 @@ interface OrganizationService {
suspend fun getOrganizationKeys(
organizationId: String,
): Result<OrganizationKeysResponseJson>
/**
* Request organization verified domain details for an [email] needed for SSO
* requests.
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse>
}

View File

@@ -7,9 +7,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomain
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/**
* Default implementation of [OrganizationService].
@@ -32,7 +29,6 @@ class OrganizationServiceImpl(
resetPasswordKey = resetPasswordKey,
),
)
.toResult()
override suspend fun getOrganizationDomainSsoDetails(
email: String,
@@ -42,7 +38,6 @@ class OrganizationServiceImpl(
email = email,
),
)
.toResult()
override suspend fun getOrganizationAutoEnrollStatus(
organizationIdentifier: String,
@@ -50,7 +45,6 @@ class OrganizationServiceImpl(
.getOrganizationAutoEnrollResponse(
organizationIdentifier = organizationIdentifier,
)
.toResult()
override suspend fun getOrganizationKeys(
organizationId: String,
@@ -58,15 +52,4 @@ class OrganizationServiceImpl(
.getOrganizationKeys(
organizationId = organizationId,
)
.toResult()
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse> = unauthenticatedOrganizationApi
.getVerifiedOrganizationDomainsByEmail(
body = VerifiedOrganizationDomainSsoDetailsRequest(
email = email,
),
)
.toResult()
}

View File

@@ -17,7 +17,6 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.manager.util.isSso
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext
@@ -66,7 +65,7 @@ class AuthRequestManagerImpl(
email: String,
authRequestType: AuthRequestType,
): Flow<CreateAuthRequestResult> = flow {
val initialResult = createNewAuthRequestIfNecessary(
val initialResult = createNewAuthRequest(
email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(),
)
@@ -75,6 +74,7 @@ class AuthRequestManagerImpl(
emit(CreateAuthRequestResult.Error)
return@flow
}
val authRequestResponse = initialResult.authRequestResponse
var authRequest = initialResult.authRequest
emit(CreateAuthRequestResult.Update(authRequest))
@@ -84,7 +84,7 @@ class AuthRequestManagerImpl(
newAuthRequestService
.getAuthRequestUpdate(
requestId = authRequest.id,
accessCode = initialResult.accessCode,
accessCode = authRequestResponse.accessCode,
isSso = authRequestType.isSso,
)
.map { request ->
@@ -112,8 +112,7 @@ class AuthRequestManagerImpl(
emit(
CreateAuthRequestResult.Success(
authRequest = updateAuthRequest,
privateKey = initialResult.privateKey,
accessCode = initialResult.accessCode,
authRequestResponse = authRequestResponse,
),
)
}
@@ -355,52 +354,6 @@ class AuthRequestManagerImpl(
)
}
/**
* Creates a new auth request for the given email and returns a [NewAuthRequestData].
* If the auth request type is [AuthRequestTypeJson.ADMIN_APPROVAL], check for a
* pending auth request and return it if it exists we should return that request.
*/
private suspend fun createNewAuthRequestIfNecessary(
email: String,
authRequestType: AuthRequestTypeJson,
): Result<NewAuthRequestData> {
return if (authRequestType == AuthRequestTypeJson.ADMIN_APPROVAL) {
authDiskSource
.getPendingAuthRequest(requireNotNull(activeUserId))
?.let { pendingAuthRequest ->
authRequestsService
.getAuthRequest(pendingAuthRequest.requestId)
.map {
NewAuthRequestData(
authRequest = AuthRequest(
id = it.id,
publicKey = it.publicKey,
platform = it.platform,
ipAddress = it.ipAddress,
key = it.key,
masterPasswordHash = it.masterPasswordHash,
creationDate = it.creationDate,
responseDate = it.responseDate,
requestApproved = it.requestApproved ?: false,
originUrl = it.originUrl,
fingerprint = pendingAuthRequest.requestFingerprint,
),
privateKey = pendingAuthRequest.requestPrivateKey,
accessCode = pendingAuthRequest.requestAccessCode,
)
.asSuccess()
}
.getOrNull()
}
?: createNewAuthRequest(email = email, authRequestType = authRequestType)
} else {
createNewAuthRequest(
email = email,
authRequestType = authRequestType,
)
}
}
/**
* Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
* with the [AuthRequest] and [AuthRequestResponse].
@@ -428,8 +381,6 @@ class AuthRequestManagerImpl(
pendingAuthRequest = PendingAuthRequestJson(
requestId = it.id,
requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
),
)
}
@@ -449,13 +400,7 @@ class AuthRequestManagerImpl(
fingerprint = authRequestResponse.fingerprint,
)
}
.map {
NewAuthRequestData(
authRequest = it,
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
)
}
.map { NewAuthRequestData(it, authRequestResponse) }
}
private suspend fun getFingerprintPhrase(
@@ -475,6 +420,5 @@ class AuthRequestManagerImpl(
*/
private data class NewAuthRequestData(
val authRequest: AuthRequest,
val privateKey: String,
val accessCode: String,
val authRequestResponse: AuthRequestResponse,
)

View File

@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.manager.model
import com.bitwarden.core.AuthRequestResponse
/**
* Models result of creating a new login approval request.
*/
@@ -16,8 +18,7 @@ sealed class CreateAuthRequestResult {
*/
data class Success(
val authRequest: AuthRequest,
val privateKey: String,
val accessCode: String,
val authRequestResponse: AuthRequestResponse,
) : CreateAuthRequestResult()
/**

View File

@@ -28,7 +28,6 @@ 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
@@ -330,13 +329,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
): OrganizationDomainSsoDetailsResult
/**
* Get the verified organization domain SSO details for the given [email].
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult
/**
* Prevalidates the organization identifier used in an SSO request.
*/
@@ -401,4 +393,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
/**
* Update the value of the showImportLogins status for the user.
*/
fun setShowImportLogins(showImportLogins: Boolean)
}

View File

@@ -68,7 +68,6 @@ 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
@@ -94,7 +93,6 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
@@ -626,12 +624,7 @@ class AuthRepositoryImpl(
)
}
.fold(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
else -> LoginResult.Error(errorMessage = null)
}
},
onFailure = { LoginResult.Error(errorMessage = null) },
onSuccess = { it },
)
@@ -1130,27 +1123,11 @@ class AuthRepositoryImpl(
OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = it.isSsoAvailable,
organizationIdentifier = it.organizationIdentifier,
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult = organizationService
.getVerifiedOrganizationDomainSsoDetails(
email = email,
)
.fold(
onSuccess = {
VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun prevalidateSso(
organizationIdentifier: String,
): PrevalidateSsoResult = identityService
@@ -1308,7 +1285,7 @@ class AuthRepositoryImpl(
.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = email,
name = name.takeUnless { it.isBlank() },
name = name,
receiveMarketingEmails = receiveMarketingEmails,
),
)
@@ -1350,6 +1327,11 @@ class AuthRepositoryImpl(
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
override fun setShowImportLogins(showImportLogins: Boolean) {
val userId: String = activeUserId ?: return
authDiskSource.storeShowImportLogins(userId = userId, showImportLogins = showImportLogins)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,
@@ -1514,12 +1496,9 @@ class AuthRepositoryImpl(
captchaToken = captchaToken,
)
.fold(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
LoginResult.UnofficialServerError
}
onFailure = {
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
false -> LoginResult.UnofficialServerError
else -> LoginResult.Error(errorMessage = null)
}
},

View File

@@ -28,9 +28,4 @@ sealed class LoginResult {
* There was an error while logging into an unofficial Bitwarden server.
*/
data object UnofficialServerError : LoginResult()
/**
* There was an error in validating the certificate chain for the server
*/
data object CertificateError : LoginResult()
}

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.auth.repository.model
import java.time.ZonedDateTime
/**
* Response types when checking for an email's claimed domain organization.
*/
@@ -11,12 +9,10 @@ sealed class OrganizationDomainSsoDetailsResult {
*
* @property isSsoAvailable Indicates if SSO is available for the email address.
* @property organizationIdentifier The claimed organization identifier for the email address.
* @property verifiedDate The date and time when the domain was verified.
*/
data class Success(
val isSsoAvailable: Boolean,
val organizationIdentifier: String,
val verifiedDate: ZonedDateTime?,
) : OrganizationDomainSsoDetailsResult()
/**

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail
/**
* Response types when checking for an email's claimed domain organization.
*/
sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request was successful.
*
* @property verifiedOrganizationDomainSsoDetails The verified organization domain SSO details.
*/
data class Success(
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) : VerifiedOrganizationDomainSsoDetailsResult()
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
}

View File

@@ -22,7 +22,8 @@ class BitwardenAccessibilityService : AccessibilityService() {
lateinit var processor: BitwardenAccessibilityProcessor
override fun onAccessibilityEvent(event: AccessibilityEvent) {
processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
if (rootInActiveWindow?.packageName != event.packageName) return
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source)
}
override fun onInterrupt() = Unit

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -24,13 +24,13 @@ object ActivityAccessibilityModule {
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appStateManager = appStateManager,
appForegroundManager = appForegroundManager,
lifecycleScope = lifecycleScope,
)
}

View File

@@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.onEach
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appStateManager
appForegroundManager
.appForegroundStateFlow
.onEach {
accessibilityEnabledManager.isAccessibilityEnabled =

View File

@@ -26,18 +26,18 @@ class AccessibilityCompletionManagerImpl(
.intent
?.getAutofillSelectionDataOrNull()
?: run {
activity.finishAndRemoveTask()
activity.finish()
return
}
if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) {
activity.finishAndRemoveTask()
activity.finish()
return
}
val uri = autofillSelectionData
.uri
?.toUriOrNull()
?: run {
activity.finishAndRemoveTask()
activity.finish()
return
}
@@ -47,7 +47,7 @@ class AccessibilityCompletionManagerImpl(
)
mainScope.launch {
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
activity.finish()
}
activity.finishAndRemoveTask()
}
}

View File

@@ -8,6 +8,4 @@ import android.view.accessibility.AccessibilityNodeInfo
data class FillableFields(
val usernameField: AccessibilityNodeInfo?,
val passwordFields: List<AccessibilityNodeInfo>,
) {
val hasFields: Boolean = usernameField != null && passwordFields.isNotEmpty()
}
)

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
/**
@@ -8,12 +7,7 @@ import android.view.accessibility.AccessibilityNodeInfo
*/
interface BitwardenAccessibilityProcessor {
/**
* Processes the [AccessibilityEvent] for autofill options and grant access to the current
* [AccessibilityNodeInfo] via the [rootAccessibilityNodeInfoProvider] (note that calling the
* `rootAccessibilityNodeInfoProvider` is expensive).
* Processes the [AccessibilityNodeInfo] for autofill options.
*/
fun processAccessibilityEvent(
event: AccessibilityEvent,
rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?,
)
fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?)
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.content.Context
import android.os.PowerManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.x8bit.bitwarden.R
@@ -26,48 +25,37 @@ class BitwardenAccessibilityProcessorImpl(
private val launcherPackageNameManager: LauncherPackageNameManager,
private val powerManager: PowerManager,
) : BitwardenAccessibilityProcessor {
override fun processAccessibilityEvent(
event: AccessibilityEvent,
rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?,
) {
val eventNode = event.source ?: return
override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) {
val rootNode = rootAccessibilityNodeInfo ?: return
// Ignore the event when the phone is inactive
if (!powerManager.isInteractive) return
// We skip if the system package
if (eventNode.isSystemPackage) return
// We skip any package that is unsupported
if (eventNode.shouldSkipPackage) return
// We skip any package that is a launcher
if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) {
if (rootNode.isSystemPackage) return
// We skip any package that is a launcher or unsupported
if (rootNode.shouldSkipPackage ||
launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName }
) {
// Clear the action since this event needs to be ignored completely
accessibilityAutofillManager.accessibilityAction = null
return
}
// Only process the event if the tile was clicked
val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return
// We only call for the root node once after all other checks
// have passed because it is significant performance hit
if (rootAccessibilityNodeInfoProvider()?.packageName != event.packageName) return
// Clear the action since we are now acting on it
accessibilityAutofillManager.accessibilityAction = null
when (accessibilityAction) {
is AccessibilityAction.AttemptFill -> {
handleAttemptFill(rootNode = eventNode, attemptFill = accessibilityAction)
handleAttemptFill(rootNode = rootNode, attemptFill = accessibilityAction)
}
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = eventNode)
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = rootNode)
}
}
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
accessibilityParser
.parseForUriOrPackageName(rootNode = rootNode)
?.takeIf {
accessibilityParser
.parseForFillableFields(rootNode = rootNode, uri = it)
.hasFields
}
?.let { uri ->
context.startActivity(
createAutofillSelectionIntent(

View File

@@ -8,7 +8,7 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -27,13 +27,13 @@ object ActivityAutofillModule {
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
appStateManager = appStateManager,
appForegroundManager = appForegroundManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
)

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET
import retrofit2.http.Url
@@ -16,5 +15,5 @@ interface DigitalAssetLinkApi {
@GET
suspend fun getDigitalAssetLinks(
@Url url: String,
): NetworkResult<List<DigitalAssetLinkResponseJson>>
): Result<List<DigitalAssetLinkResponseJson>>
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api.DigitalAssetLinkApi
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/**
* Primary implementation of [DigitalAssetLinkService].
@@ -19,5 +18,4 @@ class DigitalAssetLinkServiceImpl(
.getDigitalAssetLinks(
url = "$scheme$relyingParty/.well-known/assetlinks.json",
)
.toResult()
}

View File

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.onEach
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val autofillEnabledManager: AutofillEnabledManager,
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
lifecycleScope: LifecycleCoroutineScope,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
@@ -21,7 +21,7 @@ class AutofillActivityManagerImpl(
autofillManager.isAutofillSupported
init {
appStateManager
appForegroundManager
.appForegroundStateFlow
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.launchIn(lifecycleScope)

View File

@@ -38,7 +38,7 @@ fun createAutofillSelectionIntent(
.apply {
// This helps prevent a crash when using the accessibility framework
if (framework == AutofillSelectionData.Framework.ACCESSIBILITY) {
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
putExtra(
AUTOFILL_BUNDLE_KEY,

View File

@@ -74,11 +74,6 @@ interface SettingsDiskSource {
*/
var lastDatabaseSchemeChangeInstant: Instant?
/**
* Emits updates that track [lastDatabaseSchemeChangeInstant].
*/
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
/**
* Clears all the settings data for the given user.
*/
@@ -303,20 +298,4 @@ interface SettingsDiskSource {
* Emits updates that track [getShowUnlockSettingBadge] for the given [userId].
*/
fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the given [userId] has signalled they want to import logins later.
*/
fun getShowImportLoginsSettingBadge(userId: String): Boolean?
/**
* Stores the given value for whether or not the given [userId] has signalled they want to
* set import logins later, during first time usage.
*/
fun storeShowImportLoginsSettingBadge(userId: String, showBadge: Boolean?)
/**
* Emits updates that track [getShowImportLoginsSettingBadge] for the given [userId].
*/
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
}

View File

@@ -35,7 +35,6 @@ 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_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
/**
@@ -66,17 +65,12 @@ class SettingsDiskSourceImpl(
private val mutableShowUnlockSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowImportLoginsSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@@ -160,14 +154,7 @@ class SettingsDiskSourceImpl(
override var lastDatabaseSchemeChangeInstant: Instant?
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
set(value) {
putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value)
}
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
get() = mutableLastDatabaseSchemeChangeInstantFlow
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
@@ -425,24 +412,6 @@ class SettingsDiskSourceImpl(
getMutableShowUnlockSettingBadgeFlow(userId = userId)
.onSubscription { emit(getShowUnlockSettingBadge(userId)) }
override fun getShowImportLoginsSettingBadge(userId: String): Boolean? {
return getBoolean(
key = SHOW_IMPORT_LOGINS_SETTING_BADGE.appendIdentifier(userId),
)
}
override fun storeShowImportLoginsSettingBadge(userId: String, showBadge: Boolean?) {
putBoolean(
key = SHOW_IMPORT_LOGINS_SETTING_BADGE.appendIdentifier(userId),
showBadge,
)
getMutableShowImportLoginsSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsSettingBadgeFlow(userId)
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
@@ -486,11 +455,4 @@ class SettingsDiskSourceImpl(
mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> =
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}

View File

@@ -37,6 +37,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
@@ -73,6 +74,7 @@ object PlatformDiskModule {
fun provideEventDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): PlatformDatabase =
Room
.databaseBuilder(
@@ -82,7 +84,12 @@ object PlatformDiskModule {
)
.fallbackToDestructiveMigration()
.addTypeConverter(ZonedDateTimeTypeConverter())
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
.addCallback(
DatabaseSchemeCallback(
databaseSchemeManager = databaseSchemeManager,
clock = clock,
),
)
.build()
@Provides

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET
/**
@@ -10,5 +9,5 @@ import retrofit2.http.GET
interface ConfigApi {
@GET("config")
suspend fun getConfig(): NetworkResult<ConfigResponseJson>
suspend fun getConfig(): Result<ConfigResponseJson>
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson
import retrofit2.http.Body
import retrofit2.http.POST
@@ -10,7 +9,5 @@ import retrofit2.http.POST
*/
interface EventApi {
@POST("/collect")
suspend fun collectOrganizationEvents(
@Body events: List<OrganizationEventJson>,
): NetworkResult<Unit>
suspend fun collectOrganizationEvents(@Body events: List<OrganizationEventJson>): Result<Unit>
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import retrofit2.http.Body
import retrofit2.http.PUT
@@ -14,5 +13,5 @@ interface PushApi {
suspend fun putDeviceToken(
@Path("appId") appId: String,
@Body body: PushTokenRequest,
): NetworkResult<Unit>
): Result<Unit>
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type
/**
* A [CallAdapter] for wrapping network requests into [NetworkResult].
*/
class NetworkResultCallAdapter<T>(
private val successType: Type,
) : CallAdapter<T, Call<NetworkResult<T>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<NetworkResult<T>> = NetworkResultCall(call, successType)
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import okhttp3.Request
import okio.IOException
import okio.Timeout
@@ -17,36 +18,33 @@ import java.lang.reflect.Type
private const val NO_CONTENT_RESPONSE_CODE: Int = 204
/**
* A [Call] for wrapping a network request into a [NetworkResult].
* A [Call] for wrapping a network request into a [Result].
*/
@Suppress("TooManyFunctions")
class NetworkResultCall<T>(
class ResultCall<T>(
private val backingCall: Call<T>,
private val successType: Type,
) : Call<NetworkResult<T>> {
) : Call<Result<T>> {
override fun cancel(): Unit = backingCall.cancel()
override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(backingCall, successType)
override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)
override fun enqueue(callback: Callback<NetworkResult<T>>): Unit = backingCall.enqueue(
override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(
this@NetworkResultCall,
Response.success(response.toNetworkResult()),
)
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@NetworkResultCall, Response.success(t.toFailure()))
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
}
},
)
@Suppress("TooGenericExceptionCaught")
override fun execute(): Response<NetworkResult<T>> =
override fun execute(): Response<Result<T>> =
try {
Response.success(backingCall.execute().toNetworkResult())
Response.success(backingCall.execute().toResult())
} catch (ioException: IOException) {
Response.success(ioException.toFailure())
} catch (runtimeException: RuntimeException) {
@@ -62,18 +60,16 @@ class NetworkResultCall<T>(
override fun timeout(): Timeout = backingCall.timeout()
/**
* Synchronously send the request and return its response as a [NetworkResult].
* Synchronously send the request and return its response as a [Result].
*/
fun executeForResult(): NetworkResult<T> = requireNotNull(execute().body())
fun executeForResult(): Result<T> = requireNotNull(execute().body())
private fun Throwable.toFailure(): NetworkResult<T> {
// We rebuild the URL without query params, we do not want to log those
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
Timber.w(this, "Network Error: $url")
return NetworkResult.Failure(this)
}
private fun Throwable.toFailure(): Result<T> =
this
.also { Timber.w(it, "Network Error: ${backingCall.request().url}") }
.asFailure()
private fun Response<T>.toNetworkResult(): NetworkResult<T> =
private fun Response<T>.toResult(): Result<T> =
if (!this.isSuccessful) {
HttpException(this).toFailure()
} else {
@@ -81,11 +77,11 @@ class NetworkResultCall<T>(
@Suppress("UNCHECKED_CAST")
when {
// We got a nonnull T as the body, just return it.
body != null -> NetworkResult.Success(body)
body != null -> body.asSuccess()
// We expected the body to be null since the successType is Unit, just return Unit.
successType == Unit::class.java -> NetworkResult.Success(Unit as T)
successType == Unit::class.java -> (Unit as T).asSuccess()
// We allow null for 204's, just return null.
this.code() == NO_CONTENT_RESPONSE_CODE -> NetworkResult.Success(null as T)
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
// All other null bodies result in an error.
else -> IllegalStateException("Unexpected null body!").toFailure()
}

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core
import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type
/**
* A [CallAdapter] for wrapping network requests into [kotlin.Result].
*/
class ResultCallAdapter<T>(
private val successType: Type,
) : CallAdapter<T, Call<Result<T>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
@@ -8,9 +7,9 @@ import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* A [CallAdapter.Factory] for wrapping network requests into [NetworkResult].
* A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
*/
class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
class ResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
@@ -19,13 +18,13 @@ class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
check(returnType is ParameterizedType) { "$returnType must be parameterized" }
val containerType = getParameterUpperBound(0, returnType)
if (getRawType(containerType) != NetworkResult::class.java) return null
if (getRawType(containerType) != Result::class.java) return null
check(containerType is ParameterizedType) { "$containerType must be parameterized" }
val requestType = getParameterUpperBound(0, containerType)
return if (getRawType(returnType) == Call::class.java) {
NetworkResultCallAdapter<Any>(successType = requestType)
ResultCallAdapter<Any>(successType = requestType)
} else {
null
}

View File

@@ -1,21 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.model
import androidx.annotation.Keep
/**
* A wrapper class for a network result for type [T]. If the network request is successful, the
* response will be a [Success] containing the data. If the network request is a failure, the
* response will be a [Failure] containing the [Throwable].
*/
@Keep
sealed class NetworkResult<out T> {
/**
* A successful network result with the relevant [T] data.
*/
data class Success<T>(val value: T) : NetworkResult<T>()
/**
* A failed network result with the relevant [throwable] error.
*/
data class Failure(val throwable: Throwable) : NetworkResult<Nothing>()
}

View File

@@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
@@ -105,7 +105,7 @@ class RetrofitsImpl(
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
Retrofit.Builder()
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
.addCallAdapterFactory(ResultCallAdapterFactory())
.client(baseOkHttpClient)
}

View File

@@ -10,13 +10,11 @@ import kotlinx.serialization.encoding.Encoder
/**
* Base [KSerializer] for mapping an [Enum] with possible values given by [values] to/from integer
* values, which should be specified using [SerialName]. If a [default] value is provided, it will
* be used when an unknown value is provided.
* values, which should be specified using [SerialName].
*/
@Suppress("UnnecessaryAbstractClass")
abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
private val values: Array<T>,
private val default: T? = null,
) : KSerializer<T> {
override val descriptor: SerialDescriptor
@@ -27,9 +25,7 @@ abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
override fun deserialize(decoder: Decoder): T {
val decodedValue = decoder.decodeInt().toString()
return values.firstOrNull { it.serialNameAnnotation?.value == decodedValue }
?: default
?: throw IllegalArgumentException("Unknown value $decodedValue")
return values.first { it.serialNameAnnotation?.value == decodedValue }
}
override fun serialize(encoder: Encoder, value: T) {

View File

@@ -2,8 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig().toResult()
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig()
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.api.EventApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/**
* The default implementation of the [EventService].
@@ -12,5 +11,5 @@ class EventServiceImpl(
) : EventService {
override suspend fun sendOrganizationEvents(
events: List<OrganizationEventJson>,
): Result<Unit> = eventApi.collectOrganizationEvents(events = events).toResult()
): Result<Unit> = eventApi.collectOrganizationEvents(events = events)
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.api.PushApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class PushServiceImpl(
private val pushApi: PushApi,
@@ -16,5 +15,4 @@ class PushServiceImpl(
appId = appId,
body = body,
)
.toResult()
}

View File

@@ -1,22 +1,21 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util
import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCall
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCall
import retrofit2.Call
/**
* Synchronously executes the [Call] and returns the [NetworkResult].
* Synchronously executes the [Call] and returns the [Result].
*/
inline fun <reified T : Any> Call<T>.executeForNetworkResult(): NetworkResult<T> =
inline fun <reified T : Any> Call<T>.executeForResult(): Result<T> =
this
.toNetworkResultCall()
.toResultCall()
.executeForResult()
/**
* Wraps the existing [Call] in a [NetworkResultCall].
* Wraps the existing [Call] in a [ResultCall].
*/
inline fun <reified T : Any> Call<T>.toNetworkResultCall(): NetworkResultCall<T> =
NetworkResultCall(
inline fun <reified T : Any> Call<T>.toResultCall(): ResultCall<T> =
ResultCall(
backingCall = this,
successType = T::class.java,
)

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
/**
* Converts the [NetworkResult] to a [Result].
*/
fun <T> NetworkResult<T>.toResult(): Result<T> =
when (this) {
is NetworkResult.Failure -> this.throwable.asFailure()
is NetworkResult.Success -> this.value.asSuccess()
}

View File

@@ -3,9 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util
import okio.ByteString.Companion.decodeBase64
import java.net.UnknownHostException
import java.nio.charset.Charset
import java.security.cert.CertPathValidatorException
import java.util.Base64
import javax.net.ssl.SSLHandshakeException
/**
* Base 64 encode the string as well as make special modifications required by the backend:
@@ -43,12 +41,3 @@ fun Throwable?.isNoConnectionError(): Boolean {
return this is UnknownHostException ||
this?.cause?.isNoConnectionError() ?: false
}
/**
* Returns true if the throwable represents a SSL handshake error.
*/
fun Throwable?.isSslHandShakeError(): Boolean {
return this is SSLHandshakeException ||
this is CertPathValidatorException ||
this?.cause?.isSslHandShakeError() ?: false
}

View File

@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.StateFlow
/**
* A manager for tracking app foreground state changes.
*/
interface AppForegroundManager {
/**
* Emits whenever there are changes to the app foreground state.
*/
val appForegroundStateFlow: StateFlow<AppForegroundState>
}

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.platform.manager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Primary implementation of [AppForegroundManager].
*/
class AppForegroundManagerImpl(
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) : AppForegroundManager {
private val mutableAppForegroundStateFlow =
MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow()
init {
processLifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
}
override fun onStop(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
}
},
)
}
}

View File

@@ -1,24 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.StateFlow
/**
* A manager for tracking app foreground state changes.
*/
interface AppStateManager {
/**
* Emits whenever there are changes to the app creation state.
*
* This is required because the [BitwardenAccessibilityService] will keep the app process alive
* when the app would otherwise be destroyed.
*/
val appCreatedStateFlow: StateFlow<AppCreationState>
/**
* Emits whenever there are changes to the app foreground state.
*/
val appForegroundStateFlow: StateFlow<AppForegroundState>
}

View File

@@ -1,72 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import android.app.Activity
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Primary implementation of [AppStateManager].
*/
class AppStateManagerImpl(
application: Application,
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) : AppStateManager {
private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED)
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appCreatedStateFlow: StateFlow<AppCreationState>
get() = mutableAppCreationStateFlow.asStateFlow()
override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow()
init {
application.registerActivityLifecycleCallbacks(AppCreationCallback())
processLifecycleOwner.lifecycle.addObserver(AppForegroundObserver())
}
private inner class AppForegroundObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
}
override fun onStop(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
}
}
private inner class AppCreationCallback : Application.ActivityLifecycleCallbacks {
private var activityCount: Int = 0
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activityCount++
// Always be in a created state if we have an activity
mutableAppCreationStateFlow.value = AppCreationState.CREATED
}
override fun onActivityDestroyed(activity: Activity) {
activityCount--
if (activityCount == 0 && !activity.isChangingConfigurations) {
mutableAppCreationStateFlow.value = AppCreationState.DESTROYED
}
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
}
}

View File

@@ -1,18 +1,17 @@
package com.x8bit.bitwarden.data.platform.manager
import kotlinx.coroutines.flow.Flow
import java.time.Instant
/**
* Manager for tracking changes to database scheme(s).
*/
interface DatabaseSchemeManager {
/**
* Clears the sync state for all users and emits on the [databaseSchemeChangeFlow].
*/
fun clearSyncState()
/**
* Emits whenever the sync state hs been cleared.
* The instant of the last database schema change performed on the database, if any.
*
* There is only a single scheme change instant tracked for all database schemes. It is expected
* that a scheme change to any database will update this value and trigger a sync.
*/
val databaseSchemeChangeFlow: Flow<Unit>
var lastDatabaseSchemeChangeInstant: Instant?
}

View File

@@ -1,27 +1,17 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.time.Instant
/**
* Primary implementation of [DatabaseSchemeManager].
*/
class DatabaseSchemeManagerImpl(
val authDiskSource: AuthDiskSource,
val settingsDiskSource: SettingsDiskSource,
) : DatabaseSchemeManager {
private val mutableSharedFlow: MutableSharedFlow<Unit> = bufferedMutableSharedFlow()
override fun clearSyncState() {
authDiskSource.userState?.accounts?.forEach { (userId, _) ->
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
override var lastDatabaseSchemeChangeInstant: Instant?
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
set(value) {
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
}
mutableSharedFlow.tryEmit(Unit)
}
override val databaseSchemeChangeFlow: Flow<Unit> = mutableSharedFlow.asSharedFlow()
}

View File

@@ -44,26 +44,4 @@ interface FirstTimeActionManager {
* a default configuration.
*/
val currentOrDefaultUserFirstTimeState: FirstTimeState
/**
* Stores the given value for whether or not the active user has signalled they want to
* set up unlock options later, during onboarding.
*/
fun storeShowUnlockSettingBadge(showBadge: Boolean)
/**
* Stores the given value for whether or not the active user has signalled they want to
* enable autofill later, during onboarding.
*/
fun storeShowAutoFillSettingBadge(showBadge: Boolean)
/**
* Update the value of the showImportLogins status for the active user.
*/
fun storeShowImportLogins(showImportLogins: Boolean)
/**
* Update the value of the showImportLoginsSettingsBadge status for the active user.
*/
fun storeShowImportLoginsSettingsBadge(showBadge: Boolean)
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager
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.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
@@ -31,7 +30,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val featureFlagManager: FeatureFlagManager,
private val autofillEnabledManager: AutofillEnabledManager,
) : FirstTimeActionManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
@@ -80,7 +78,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
.filterNotNull()
.flatMapLatest {
// Can be expanded to support multiple autofill settings
getShowAutofillSettingBadgeFlowInternal(userId = it)
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = it)
.map { showAutofillBadge ->
listOfNotNull(showAutofillBadge)
}
@@ -101,11 +99,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
.filterNotNull()
.flatMapLatest {
combine(
getShowImportLoginsSettingBadgeFlowInternal(userId = it),
getShowImportLoginsFlowInternal(userId = it),
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
) { showImportLogins, importLoginsEnabled ->
val shouldShowImportLoginsSettings = showImportLogins && importLoginsEnabled
listOf(shouldShowImportLoginsSettings)
val shouldShowImportLogins = showImportLogins && importLoginsEnabled
listOf(shouldShowImportLogins)
}
.map { list ->
list.count { showImportLogins -> showImportLogins }
@@ -130,15 +128,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
listOf(
getShowImportLoginsFlowInternal(userId = activeUserId),
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = activeUserId),
),
) {
FirstTimeState(
showImportLoginsCard = it[0],
showSetupUnlockCard = it[1],
showSetupAutofillCard = it[2],
showImportLoginsCardInSettings = it[3],
)
}
}
@@ -148,12 +144,24 @@ class FirstTimeActionManagerImpl @Inject constructor(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
showImportLoginsCardInSettings = null,
),
)
}
.distinctUntilChanged()
/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
*/
private fun getShowImportLoginsFlowInternal(userId: String): Flow<Boolean> {
return authDiskSource.getShowImportLoginsFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: true && ciphers.isEmpty()
}
}
/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
@@ -167,94 +175,12 @@ class FirstTimeActionManagerImpl @Inject constructor(
FirstTimeState(
showImportLoginsCard = authDiskSource.getShowImportLogins(it),
showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it),
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
showImportLoginsCardInSettings = settingsDiskSource
.getShowImportLoginsSettingBadge(it),
showSetupAutofillCard = settingsDiskSource.getShowAutoFillSettingBadge(it),
)
}
?: FirstTimeState(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
showImportLoginsCardInSettings = null,
)
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
settingsDiskSource.storeShowUnlockSettingBadge(
userId = activeUserId,
showBadge = showBadge,
)
}
override fun storeShowAutoFillSettingBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
settingsDiskSource.storeShowAutoFillSettingBadge(
userId = activeUserId,
showBadge = showBadge,
)
}
override fun storeShowImportLogins(showImportLogins: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
authDiskSource.storeShowImportLogins(
userId = activeUserId,
showImportLogins = showImportLogins,
)
}
override fun storeShowImportLoginsSettingsBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
settingsDiskSource.storeShowImportLoginsSettingBadge(
userId = activeUserId,
showBadge = showBadge,
)
}
/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
*/
private fun getShowImportLoginsFlowInternal(userId: String): Flow<Boolean> {
return authDiskSource
.getShowImportLoginsFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: true && ciphers.isEmpty()
}
}
/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
*/
private fun getShowImportLoginsSettingBadgeFlowInternal(userId: String): Flow<Boolean> {
return settingsDiskSource
.getShowImportLoginsSettingBadgeFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: false && ciphers.isEmpty()
}
}
/**
* Internal implementation to get a flow of the showAutofill value which takes
* into account if autofill is already enabled globally.
*/
private fun getShowAutofillSettingBadgeFlowInternal(userId: String): Flow<Boolean> {
return settingsDiskSource
.getShowAutoFillSettingBadgeFlow(userId)
.combine(
autofillEnabledManager.isAutofillEnabledStateFlow,
) { showAutofill, autofillEnabled ->
showAutofill ?: false && !autofillEnabled
}
}
private fun getShowAutofillSettingBadgeInternal(userId: String): Boolean {
return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false &&
!autofillEnabledManager.isAutofillEnabled
}
}

View File

@@ -6,7 +6,6 @@ import androidx.core.content.getSystemService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
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
@@ -15,8 +14,8 @@ import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.Refres
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
@@ -81,9 +80,8 @@ object PlatformManagerModule {
@Provides
@Singleton
fun provideAppStateManager(
application: Application,
): AppStateManager = AppStateManagerImpl(application = application)
fun provideAppForegroundManager(): AppForegroundManager =
AppForegroundManagerImpl()
@Provides
@Singleton
@@ -269,11 +267,11 @@ object PlatformManagerModule {
@Singleton
fun provideRestrictionManager(
@ApplicationContext context: Context,
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
): RestrictionManager = RestrictionManagerImpl(
appStateManager = appStateManager,
appForegroundManager = appForegroundManager,
dispatcherManager = dispatcherManager,
context = context,
environmentRepository = environmentRepository,
@@ -294,23 +292,19 @@ object PlatformManagerModule {
vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
autofillEnabledManager: AutofillEnabledManager,
): FirstTimeActionManager = FirstTimeActionManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
autofillEnabledManager = autofillEnabledManager,
)
@Provides
@Singleton
fun provideDatabaseSchemeManager(
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
)
}

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Represents the creation state of the app.
*/
enum class AppCreationState {
/**
* Denotes that the app is currently created.
*/
CREATED,
/**
* Denotes that the app is currently destroyed.
*/
DESTROYED,
}

View File

@@ -5,7 +5,6 @@ package com.x8bit.bitwarden.data.platform.manager.model
*/
data class FirstTimeState(
val showImportLoginsCard: Boolean,
val showImportLoginsCardInSettings: Boolean,
val showSetupUnlockCard: Boolean,
val showSetupAutofillCard: Boolean,
) {
@@ -17,11 +16,9 @@ data class FirstTimeState(
showImportLoginsCard: Boolean? = null,
showSetupUnlockCard: Boolean? = null,
showSetupAutofillCard: Boolean? = null,
showImportLoginsCardInSettings: Boolean? = null,
) : this(
showImportLoginsCard = showImportLoginsCard ?: true,
showSetupUnlockCard = showSetupUnlockCard ?: false,
showSetupAutofillCard = showSetupAutofillCard ?: false,
showImportLoginsCardInSettings = showImportLoginsCardInSettings ?: false,
)
}

View File

@@ -32,7 +32,6 @@ sealed class FlagKey<out T : Any> {
OnboardingCarousel,
ImportLoginsFlow,
SshKeyCipherItems,
VerifiedSsoDomainEndpoint,
)
}
}
@@ -43,7 +42,7 @@ sealed class FlagKey<out T : Any> {
data object AuthenticatorSync : FlagKey<Boolean>() {
override val keyName: String = "enable-authenticator-sync-android"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
override val isRemotelyConfigured: Boolean = false
}
/**
@@ -90,14 +89,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key for the new verified SSO domain endpoint feature.
*/
data object VerifiedSsoDomainEndpoint : FlagKey<Boolean>() {
override val keyName: String = "pm-12337-refactor-sso-details-endpoint"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the key for a [Boolean] flag to be used in tests.

View File

@@ -6,7 +6,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.RestrictionsManager
import android.os.Bundle
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
* The default implementation of the [RestrictionManager].
*/
class RestrictionManagerImpl(
appStateManager: AppStateManager,
appForegroundManager: AppForegroundManager,
dispatcherManager: DispatcherManager,
private val context: Context,
private val environmentRepository: EnvironmentRepository,
@@ -31,7 +31,7 @@ class RestrictionManagerImpl(
private var isReceiverRegistered = false
init {
appStateManager
appForegroundManager
.appForegroundStateFlow
.onEach {
when (it) {

View File

@@ -25,7 +25,6 @@ import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* Default implementation of [AuthenticatorBridgeProcessor].
@@ -94,13 +93,7 @@ class AuthenticatorBridgeProcessorImpl(
}
override fun syncAccounts() {
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: run {
Timber.e(
t = IllegalStateException(),
message = "Unable to sync accounts when symmetricEncryptionKeyData is null.",
)
return
}
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return
scope.launch {
// Encrypt the shared account data with the symmetric key:
val encryptedSharedAccountData = authenticatorBridgeRepository
@@ -117,31 +110,14 @@ class AuthenticatorBridgeProcessorImpl(
}
override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean {
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: run {
Timber.e(
t = IllegalStateException(),
message = "Unable to start add TOTP item flow when " +
"symmetricEncryptionKeyData is null.",
)
return false
}
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return false
val intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext)
val totpData = data.decrypt(symmetricEncryptionKey)
.onFailure {
Timber.e(t = it, message = "Unable to decrypt TOTP data.")
return false
}
.getOrNull()
?.totpUri
?.toUri()
?.getTotpDataOrNull()
?: run {
Timber.e(
t = IllegalStateException(),
message = "Unable to parse TOTP URI.",
)
return false
}
?: return false
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData
applicationContext.startActivity(intent)
return true

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