Compare commits

..

1 Commits

606 changed files with 10754 additions and 30871 deletions

View File

@@ -1,13 +0,0 @@
name: Build Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -68,7 +68,7 @@ jobs:
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
with:
bundler-cache: true
@@ -85,7 +85,7 @@ jobs:
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: failure()
with:
name: test-reports
@@ -106,7 +106,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
with:
bundler-cache: true
@@ -253,7 +253,7 @@ jobs:
- name: Upload release Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
@@ -261,7 +261,7 @@ jobs:
- name: Upload beta Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
@@ -269,7 +269,7 @@ jobs:
- name: Upload release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
@@ -277,7 +277,7 @@ jobs:
- name: Upload beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
@@ -286,7 +286,7 @@ jobs:
# When building variants other than 'prod'
- name: Upload debug .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
@@ -324,7 +324,7 @@ jobs:
- name: Upload .apk SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
@@ -332,7 +332,7 @@ jobs:
- name: Upload .apk SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
@@ -340,7 +340,7 @@ jobs:
- name: Upload .aab SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
@@ -348,7 +348,7 @@ jobs:
- name: Upload .aab SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
@@ -356,7 +356,7 @@ jobs:
- name: Upload .apk SHA file for debug
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
@@ -405,7 +405,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
with:
bundler-cache: true
@@ -515,7 +515,7 @@ jobs:
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
@@ -527,14 +527,14 @@ jobs:
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Upload F-Droid Beta .apk artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
@@ -546,7 +546,7 @@ jobs:
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt

View File

@@ -1,13 +0,0 @@
name: Crowdin Sync - Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -36,7 +36,7 @@ jobs:
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Download translations
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -1,13 +0,0 @@
name: Crowdin Push - Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -29,7 +29,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -95,7 +95,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
with:
tag_name: "v${{ inputs.version-name }}"
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"

View File

@@ -1,13 +0,0 @@
name: Scan Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -34,7 +34,7 @@ jobs:
--output-path .
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: cx_result.sarif
@@ -58,4 +58,3 @@ jobs:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -4,6 +4,8 @@ on:
workflow_dispatch:
pull_request_target:
types: [opened, synchronize]
merge_group:
types: [checks_requested]
jobs:
check-run:
@@ -41,7 +43,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: cx_result.sarif
@@ -68,4 +70,3 @@ jobs:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -1,13 +0,0 @@
name: Test Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -51,7 +51,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
with:
bundler-cache: true
@@ -74,7 +74,7 @@ jobs:
bundle exec fastlane check
- name: Upload test reports
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: always()
with:
name: test-reports
@@ -90,19 +90,17 @@ jobs:
contents: read
issues: write
pull-requests: write
if: success()
if: always()
steps:
- name: Download test artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: test-reports
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
if: github.event_name == 'push' || github.event_name == 'pull_request'
continue-on-error: true
with:
os: linux
@@ -110,7 +108,7 @@ jobs:
fail_ci_if_error: true
- name: Comment PR if tests failed
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
if: steps.upload-to-codecov.outcome == 'failure'
env:
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1040.0)
aws-sdk-core (3.216.0)
aws-partitions (1.1027.0)
aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.178.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.176.1)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -68,7 +68,7 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastimage (2.3.1)
fastlane (2.226.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
@@ -111,7 +111,7 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-firebase_app_distribution (0.10.0)
fastlane-plugin-firebase_app_distribution (0.9.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
@@ -163,7 +163,7 @@ GEM
httpclient (2.8.3)
jmespath (1.6.2)
json (2.9.1)
jwt (2.10.1)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
@@ -174,7 +174,7 @@ GEM
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
representable (3.2.0)
@@ -185,7 +185,7 @@ GEM
rexml (3.4.0)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)

View File

@@ -325,7 +325,6 @@ kover {
"*_*Factory\$*",
"*.Hilt_*",
"*_HiltModules",
"*_HiltModules*",
"*_HiltModules\$*",
"*_Impl",
"*_Impl\$*",

View File

@@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
Note that each build type uses a different value for knownCerts.
@@ -320,11 +320,6 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<!-- To Query Chrome Beta: -->
<package android:name="com.chrome.beta" />
<!-- To Query Chrome Stable: -->
<package android:name="com.android.chrome" />
</queries>
</manifest>

View File

@@ -12,20 +12,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "net.quetta.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
}
]
}
},
{
"type": "android",
"info": {
@@ -50,18 +36,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {
@@ -85,6 +59,34 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.fennec_dos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
},
{
"build": "release",
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.mulch",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
}
]
}
}
]
}

View File

@@ -4,8 +4,8 @@ import android.app.Application
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

View File

@@ -11,7 +11,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -20,7 +19,6 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
@@ -55,7 +53,6 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
@Suppress("LongMethod")
override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
@@ -69,14 +66,13 @@ class MainActivity : AppCompatActivity() {
)
}
// Within the app the language and theme will change dynamically and will be managed by the
// OS, but we need to ensure we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
// Within the app the language will change dynamically and will be managed
// by the OS, but we need to ensure we properly set the language when
// upgrading from older versions that handle this differently.
settingsRepository.appLanguage.localeName?.let { localeName ->
val localeList = LocaleListCompat.forLanguageTags(localeName)
AppCompatDelegate.setApplicationLocales(localeList)
}
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
@@ -98,29 +94,10 @@ class MainActivity : AppCompatActivity() {
)
.show()
}
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(event.localeName),
)
}
is MainEvent.UpdateAppTheme -> {
AppCompatDelegate.setDefaultNightMode(event.osTheme)
}
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{
mainViewModel.trySendAction(
MainAction.ResumeScreenDataReceived(it),
)
}
},
)
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },

View File

@@ -13,15 +13,13 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CreateCredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@@ -73,7 +71,6 @@ class MainViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val savedStateHandle: SavedStateHandle,
private val appResumeManager: AppResumeManager,
private val clock: Clock,
) : BaseViewModel<MainState, MainEvent, MainAction>(
initialState = MainState(
@@ -111,11 +108,6 @@ class MainViewModel @Inject constructor(
.appThemeStateFlow
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
.launchIn(viewModelScope)
settingsRepository
.appLanguageStateFlow
.map { MainEvent.UpdateAppLocale(it.localeName) }
.onEach(::sendEvent)
.launchIn(viewModelScope)
settingsRepository
.isScreenCaptureAllowedStateFlow
@@ -188,14 +180,6 @@ class MainViewModel @Inject constructor(
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
}
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
else -> appResumeManager.setResumeScreen(data)
}
}
@@ -227,7 +211,6 @@ class MainViewModel @Inject constructor(
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
mutableStateFlow.update { it.copy(theme = action.theme) }
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
}
private fun handleVaultUnlockStateChange() {
@@ -274,7 +257,7 @@ class MainViewModel @Inject constructor(
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
@@ -335,31 +318,25 @@ class MainViewModel @Inject constructor(
)
}
fido2CreateCredentialRequestData != null -> {
fido2CredentialRequestData != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CreateCredentialRequestData.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
fido2CredentialManager.isUserVerified = false
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
fido2CreateCredentialRequest = fido2CredentialRequestData,
)
// Switch accounts if the selected user is not the active user.
if (authRepository.activeUserId != null &&
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
authRepository.activeUserId != fido2CredentialRequestData.userId
) {
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
authRepository.switchAccount(fido2CredentialRequestData.userId)
}
}
fido2CredentialAssertionRequest != null -> {
// If device biometric verification was performed as part of single-tap
// authentication, set the user's verification state to the device result.
// Otherwise, retain the verification state as-is.
fido2CredentialAssertionRequest.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = fido2CredentialAssertionRequest,
@@ -466,11 +443,6 @@ sealed class MainAction {
*/
data object OpenDebugMenu : MainAction()
/**
* Receive event to save the app resume screen
*/
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
@@ -546,18 +518,4 @@ sealed class MainEvent {
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : MainEvent()
/**
* Indicates that the app language has been updated.
*/
data class UpdateAppLocale(
val localeName: String?,
) : MainEvent()
/**
* Indicates that the app theme has been updated.
*/
data class UpdateAppTheme(
val osTheme: Int,
) : MainEvent()
}

View File

@@ -7,7 +7,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJso
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import java.time.Instant
/**
* Primary access point for disk information.
@@ -353,14 +352,4 @@ interface AuthDiskSource {
* Stores the new device notice state for the given [userId].
*/
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
/**
* Gets the last lock timestamp for the given [userId].
*/
fun getLastLockTimestamp(userId: String): Instant?
/**
* Stores the last lock timestamp for the given [userId].
*/
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)
}

View File

@@ -15,8 +15,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.util.UUID
// These keys should be encrypted
@@ -50,7 +50,6 @@ private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
/**
* Primary implementation of [AuthDiskSource].
@@ -156,7 +155,6 @@ class AuthDiskSourceImpl(
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
@@ -505,19 +503,6 @@ class AuthDiskSourceImpl(
)
}
override fun getLastLockTimestamp(userId: String): Instant? {
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
Instant.ofEpochMilli(it)
}
}
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
putLong(
key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId),
value = lastLockTimestamp?.toEpochMilli(),
)
}
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()

View File

@@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable
* Represents URLs for various Bitwarden domains.
*
* @property base The overall base URL.
* @property keyUri A Uri containing the alias and host of the key used for mutual TLS.
* @property api Separate base URL for the "/api" domain (if applicable).
* @property identity Separate base URL for the "/identity" domain (if applicable).
* @property icon Separate base URL for the icon domain (if applicable).
@@ -20,9 +19,6 @@ data class EnvironmentUrlDataJson(
@SerialName("base")
val base: String,
@SerialName("keyUri")
val keyUri: String? = null,
@SerialName("api")
val api: String? = null,
@@ -55,7 +51,6 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.com",
keyUri = null,
api = "https://api.bitwarden.com",
identity = "https://identity.bitwarden.com",
icon = "https://icons.bitwarden.net",
@@ -76,7 +71,6 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.eu",
keyUri = null,
api = "https://api.bitwarden.eu",
identity = "https://identity.bitwarden.eu",
icon = "https://icons.bitwarden.eu",

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.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
@@ -29,9 +28,4 @@ interface UnauthenticatedAccountsApi {
@Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): NetworkResult<Unit>
@POST("/accounts/resend-new-device-otp")
suspend fun resendNewDeviceOtp(
@Body body: ResendNewDeviceOtpRequestJson,
): NetworkResult<Unit>
}

View File

@@ -47,13 +47,12 @@ interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?,
@Field(value = "newDeviceOtp") newDeviceOtp: String?,
): NetworkResult<GetTokenResponseJson.Success>
@GET("/sso/prevalidate")
suspend fun prevalidateSso(
@Query("domainHint") organizationIdentifier: String,
): NetworkResult<PrevalidateSsoResponseJson.Success>
): NetworkResult<PrevalidateSsoResponseJson>
/**
* This call needs to be synchronous so we need it to return a [Call] directly. The identity

View File

@@ -21,7 +21,5 @@ enum class AuthRequestTypeJson {
}
@Keep
private class AuthRequestTypeSerializer : BaseEnumeratedIntSerializer<AuthRequestTypeJson>(
className = "AuthRequestTypeJson",
values = AuthRequestTypeJson.entries.toTypedArray(),
)
private class AuthRequestTypeSerializer :
BaseEnumeratedIntSerializer<AuthRequestTypeJson>(AuthRequestTypeJson.entries.toTypedArray())

View File

@@ -107,28 +107,6 @@ sealed class GetTokenResponseJson {
val errorMessage: String?
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
/**
* The type of invalid responses that can be received.
*/
sealed class InvalidType {
/**
* Represents an invalid response indicating that a new device verification is required.
*/
data object NewDeviceVerification : InvalidType()
/**
* Represents generic invalid response
*/
data object GenericInvalid : InvalidType()
}
val invalidType: InvalidType
get() = if (errorMessage?.lowercase() == "new device verification required") {
InvalidType.NewDeviceVerification
} else {
InvalidType.GenericInvalid
}
/**
* The error body of an invalid request containing a message.
*/

View File

@@ -18,7 +18,5 @@ enum class KdfTypeJson {
}
@Keep
private class KdfTypeSerializer : BaseEnumeratedIntSerializer<KdfTypeJson>(
className = "KdfTypeJson",
values = KdfTypeJson.entries.toTypedArray(),
)
private class KdfTypeSerializer :
BaseEnumeratedIntSerializer<KdfTypeJson>(KdfTypeJson.entries.toTypedArray())

View File

@@ -7,20 +7,6 @@ import kotlinx.serialization.Serializable
* Response body from the SSO prevalidate request.
*/
@Serializable
sealed class PrevalidateSsoResponseJson {
/**
* Models json body of a successful response.
*/
@Serializable
data class Success(
@SerialName("token") val token: String?,
) : PrevalidateSsoResponseJson()
/**
* Models json body of an error response.
*/
@Serializable
data class Error(
@SerialName("message") val message: String?,
) : PrevalidateSsoResponseJson()
}
data class PrevalidateSsoResponseJson(
@SerialName("token") val token: String?,
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Hold the information necessary to resend the email with the
* new device verification code.
*
* @property email The user's email address.
* @property passwordHash The master password hash
*/
@Serializable
data class ResendNewDeviceOtpRequestJson(
@SerialName("Email")
val email: String,
@SerialName("MasterPasswordHash")
val passwordHash: String?,
)

View File

@@ -39,7 +39,5 @@ enum class TwoFactorAuthMethod(val value: UInt) {
}
@Keep
private class TwoFactorAuthMethodSerializer : BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(
className = "TwoFactorAuthMethod",
values = TwoFactorAuthMethod.entries.toTypedArray(),
)
private class TwoFactorAuthMethodSerializer :
BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(TwoFactorAuthMethod.entries.toTypedArray())

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyReq
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
@@ -53,11 +52,6 @@ interface AccountsService {
*/
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
/**
* Resend the email with the verification code for new devices
*/
suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit>
/**
* Reset the password.
*/

View File

@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMaster
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
@@ -115,11 +114,6 @@ class AccountsServiceImpl(
.resendVerificationCodeEmail(body = body)
.toResult()
override suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit> =
unauthenticatedAccountsApi
.resendNewDeviceOtp(body = body)
.toResult()
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
if (body.currentPasswordHash == null) {
authenticatedAccountsApi

View File

@@ -46,7 +46,6 @@ interface IdentityService {
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel? = null,
newDeviceOtp: String? = null,
): Result<GetTokenResponseJson>
/**

View File

@@ -60,7 +60,6 @@ class IdentityServiceImpl(
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel?,
newDeviceOtp: String?,
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
.getToken(
scope = "api offline_access",
@@ -80,28 +79,22 @@ class IdentityServiceImpl(
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
captchaResponse = captchaToken,
authRequestId = authModel.authRequestId,
newDeviceOtp = newDeviceOtp,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
code = 400,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = 400,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
code = 400,
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = 400,
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
code = 400,
json = json,
) ?: throw throwable
}
@Suppress("MagicNumber")
override suspend fun prevalidateSso(
organizationIdentifier: String,
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
@@ -109,15 +102,6 @@ class IdentityServiceImpl(
organizationIdentifier = organizationIdentifier,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<PrevalidateSsoResponseJson.Error>(
code = 400,
json = json,
)
?: throw throwable
}
override fun refreshTokenSynchronously(
refreshToken: String,
@@ -147,7 +131,6 @@ class IdentityServiceImpl(
?: throw throwable
}
@Suppress("MagicNumber")
override suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<SendVerificationEmailResponseJson> {
@@ -166,7 +149,6 @@ class IdentityServiceImpl(
}
}
@Suppress("MagicNumber")
override suspend fun verifyEmailRegistrationToken(
body: VerifyEmailTokenRequestJson,
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi

View File

@@ -8,7 +8,7 @@ import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.core.RegisterTdeKeyResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.AuthClient
import com.bitwarden.sdk.ClientAuth
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
@@ -17,7 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
/**
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
* [AuthClient].
* [ClientAuth].
*/
class AuthSdkSourceImpl(
sdkClientManager: SdkClientManager,

View File

@@ -230,19 +230,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
organizationIdentifier: String,
): LoginResult
/**
* Repeat the previous login attempt but this time with New Device OTP
* information. Password is included if available to unlock the vault after
* authentication. Updated access token will be reflected in [authStateFlow].
*/
suspend fun login(
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**
* Log out the current user.
*/
@@ -265,11 +252,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
suspend fun resendVerificationCodeEmail(): ResendEmailResult
/**
* Resend the email with the new device verification code.
*/
suspend fun resendNewDeviceOtp(): ResendEmailResult
/**
* Switches to the account corresponding to the given [userId] if possible.
*/
@@ -380,10 +362,8 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Get the password strength for the given [email] and [password] combo.
* If no value is passed for the [email] will use the active email of the current active
* account via the [userStateFlow].
*/
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
/**
* Validates the master password for the current logged in user.
@@ -421,7 +401,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(status: OnboardingStatus)
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
/**
* Checks if a new device notice should be displayed.

View File

@@ -17,13 +17,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
@@ -226,11 +224,6 @@ class AuthRepositoryImpl(
*/
private var resendEmailRequestJson: ResendEmailRequestJson? = null
/**
* The information necessary to resend the verification code email for new devices.
*/
private var resendNewDeviceOtpRequestJson: ResendNewDeviceOtpRequestJson? = null
private var organizationIdentifier: String? = null
/**
@@ -690,26 +683,6 @@ class AuthRepositoryImpl(
}
?: LoginResult.Error(errorMessage = null)
override suspend fun login(
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
email = email,
password = password,
authModel = it,
newDeviceOtp = newDeviceOtp,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
override suspend fun login(
email: String,
ssoCode: String,
@@ -791,16 +764,6 @@ class AuthRepositoryImpl(
}
?: ResendEmailResult.Error(message = null)
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
resendNewDeviceOtpRequestJson
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
override fun switchAccount(userId: String): SwitchAccountResult {
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
val previousActiveUserId = currentUserState.activeUserId
@@ -1125,7 +1088,6 @@ class AuthRepositoryImpl(
}
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
@@ -1200,21 +1162,13 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = {
when (it) {
is PrevalidateSsoResponseJson.Error -> {
PrevalidateSsoResult.Failure(message = it.message)
}
is PrevalidateSsoResponseJson.Success -> {
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure()
} else {
PrevalidateSsoResult.Success(token = it.token)
}
}
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure
} else {
PrevalidateSsoResult.Success(it.token)
}
},
onFailure = { PrevalidateSsoResult.Failure() },
onFailure = { PrevalidateSsoResult.Failure },
)
override fun setSsoCallbackResult(result: SsoCallbackResult) {
@@ -1241,17 +1195,12 @@ class AuthRepositoryImpl(
)
override suspend fun getPasswordStrength(
email: String?,
email: String,
password: String,
): PasswordStrengthResult =
authSdkSource
.passwordStrength(
email = email
?: userStateFlow
.value
?.activeAccount
?.email
.orEmpty(),
email = email,
password = password,
)
.fold(
@@ -1384,13 +1333,8 @@ class AuthRepositoryImpl(
)
}
override fun setOnboardingStatus(status: OnboardingStatus) {
activeUserId?.let { userId ->
authDiskSource.storeOnboardingStatus(
userId = userId,
onboardingStatus = status,
)
}
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
@@ -1428,7 +1372,6 @@ class AuthRepositoryImpl(
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.HAS_SEEN ->
newDeviceNoticeState.shouldDisplayNoticeIfSeen
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
// the user never needs to see the notice again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
@@ -1634,7 +1577,6 @@ class AuthRepositoryImpl(
deviceData: DeviceDataModel? = null,
orgIdentifier: String? = null,
captchaToken: String?,
newDeviceOtp: String? = null,
): LoginResult = identityService
.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
@@ -1642,7 +1584,6 @@ class AuthRepositoryImpl(
authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
captchaToken = captchaToken,
newDeviceOtp = newDeviceOtp,
)
.fold(
onFailure = { throwable ->
@@ -1651,7 +1592,6 @@ class AuthRepositoryImpl(
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
LoginResult.UnofficialServerError
}
else -> LoginResult.Error(errorMessage = null)
}
},
@@ -1676,22 +1616,9 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
is GetTokenResponseJson.Invalid -> {
when (loginResponse.invalidType) {
is GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification ->
handleLoginCommonNewDeviceVerification(
email = email,
authModel = authModel,
error = loginResponse.errorMessage,
)
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
LoginResult.Error(
errorMessage = loginResponse.errorMessage,
)
}
}
}
is GetTokenResponseJson.Invalid -> LoginResult.Error(
errorMessage = loginResponse.errorMessage,
)
}
},
)
@@ -1779,6 +1706,15 @@ class AuthRepositoryImpl(
)
settingsRepository.hasUserLoggedInOrCreatedAccount = true
val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) &&
!settingsRepository.getUserHasLoggedInValue(userId = userId)
if (shouldSetOnboardingStatus) {
setOnboardingStatus(
userId = userId,
status = OnboardingStatus.NOT_STARTED,
)
}
authDiskSource.userState = userStateJson
loginResponse.key?.let {
// Only set the value if it's present, since we may have set it already
@@ -1807,7 +1743,6 @@ class AuthRepositoryImpl(
twoFactorResponse = null
resendEmailRequestJson = null
twoFactorDeviceData = null
resendNewDeviceOtpRequestJson = null
settingsRepository.setDefaultsIfNecessary(userId = userId)
settingsRepository.storeUserHasLoggedInValue(userId)
vaultRepository.syncIfNecessary()
@@ -1840,24 +1775,6 @@ class AuthRepositoryImpl(
return LoginResult.TwoFactorRequired
}
/**
* A helper method that processes the
* [GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification] when logging in.
*/
private fun handleLoginCommonNewDeviceVerification(
email: String,
authModel: IdentityTokenAuthModel,
error: String?,
): LoginResult {
identityTokenAuthModel = authModel
resendNewDeviceOtpRequestJson = ResendNewDeviceOtpRequestJson(
email = email,
passwordHash = authModel.password,
)
return LoginResult.NewDeviceVerification(error)
}
/**
* Attempt to unlock the current user's vault with key connector data.
*/

View File

@@ -33,9 +33,4 @@ sealed class LoginResult {
* There was an error in validating the certificate chain for the server
*/
data object CertificateError : LoginResult()
/**
* New device verification is required
*/
data class NewDeviceVerification(val errorMessage: String?) : LoginResult()
}

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
*/
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)

View File

@@ -18,4 +18,5 @@ data class Organization(
val shouldManageResetPassword: Boolean,
val shouldUseKeyConnector: Boolean,
val role: OrganizationType,
val shouldUsersGetPremium: Boolean,
)

View File

@@ -14,7 +14,5 @@ sealed class PrevalidateSsoResult {
/**
* There was an error in prevalidation.
*/
data class Failure(
val message: String? = null,
) : PrevalidateSsoResult()
data object Failure : PrevalidateSsoResult()
}

View File

@@ -22,6 +22,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
shouldUseKeyConnector = this.shouldUseKeyConnector,
role = this.type,
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
shouldUsersGetPremium = this.shouldUsersGetPremium,
)
/**

View File

@@ -11,9 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow
class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager,
) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
value = accessibilityManager.isEnabled,
)
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
init {
accessibilityManager.addAccessibilityStateChangeListener(

View File

@@ -128,11 +128,6 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
// 2nd = Anticipation
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
),
Browser(
packageName = "org.ironfoxoss.ironfox",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
),
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
// [DEPRECATED ENTRY]
Browser(
@@ -196,6 +191,11 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
),
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),
Browser(
packageName = "us.spotco.fennec_dos",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
),
// [Section B] Entries only present here
// TODO: Test the compatibility of these with Autofill Framework

View File

@@ -8,9 +8,6 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
@@ -26,32 +23,19 @@ import dagger.hilt.android.scopes.ActivityScoped
@InstallIn(ActivityComponent::class)
object ActivityAutofillModule {
@ActivityScoped
@ActivityScopedManager
@Provides
fun provideActivityScopedChromeThirdPartyAutofillManager(
activity: Activity,
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
context = activity.baseContext,
)
@ActivityScoped
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
)
/**

View File

@@ -15,15 +15,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -57,15 +54,6 @@ object AutofillModule {
fun providesAutofillEnabledManager(): AutofillEnabledManager =
AutofillEnabledManagerImpl()
@Singleton
@Provides
fun providesChromeAutofillEnabledManager(
featureFlagManager: FeatureFlagManager,
): ChromeThirdPartyAutofillEnabledManager =
ChromeThirdPartyAutofillEnabledManagerImpl(
featureFlagManager = featureFlagManager,
)
@Singleton
@Provides
fun provideAutofillCompletionManager(

View File

@@ -13,8 +13,6 @@ import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -46,8 +44,6 @@ object Fido2ProviderModule {
fido2CredentialManager: Fido2CredentialManager,
dispatcherManager: DispatcherManager,
intentManager: IntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
featureFlagManager: FeatureFlagManager,
clock: Clock,
): Fido2ProviderProcessor =
Fido2ProviderProcessorImpl(
@@ -58,8 +54,6 @@ object Fido2ProviderModule {
fido2CredentialManager,
intentManager,
clock,
biometricsEncryptionManager,
featureFlagManager,
dispatcherManager,
)

View File

@@ -6,7 +6,6 @@ import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
@@ -23,11 +22,10 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
/**
* Primary implementation of [Fido2CredentialManager].
@@ -50,45 +48,41 @@ class Fido2CredentialManagerImpl(
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
} else {
callingAppInfo
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
fido2CreateCredentialRequest
.callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
)
}
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = fido2CreateCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = host,
assetLinkUrl = host,
),
)
?: return Fido2RegisterCredentialResult.Error
} else {
Origin.Web(fido2CreateCredentialRequest.origin)
ClientData.DefaultWithExtraData(
androidPackageName = fido2CreateCredentialRequest
.callingAppInfo
.packageName,
)
}
val assetLinkUrl = fido2CreateCredentialRequest
.origin
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
?: return Fido2RegisterCredentialResult.Error
val origin = Origin.Android(
UnverifiedAssetLink(
packageName = fido2CreateCredentialRequest.packageName,
sha256CertFingerprint = fido2CreateCredentialRequest
.callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error,
host = assetLinkUrl.toHostOrPathOrNull()
?: return Fido2RegisterCredentialResult.Error,
assetLinkUrl = assetLinkUrl,
),
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
origin = origin,
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -102,11 +96,7 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = {
Fido2RegisterCredentialResult.Error(
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
)
},
onFailure = { Fido2RegisterCredentialResult.Error },
)
}
@@ -125,10 +115,8 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
}
@@ -138,14 +126,11 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
}
@Suppress("LongMethod")
override suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
@@ -155,44 +140,22 @@ class Fido2CredentialManagerImpl(
val clientData = request.clientDataHash
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
val origin = callingAppInfo.origin
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error
val relyingPartyId = json
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
?.relyingPartyId
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
.asText(),
)
?: return Fido2CredentialAssertionResult.Error
val validateOriginResult = validateOrigin(
callingAppInfo = callingAppInfo,
relyingPartyId = relyingPartyId,
)
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
Origin.Web(request.origin)
} else {
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = hostUrl,
assetLinkUrl = hostUrl,
),
)
}
return when (validateOriginResult) {
is Fido2ValidateOriginResult.Error -> {
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
Fido2CredentialAssertionResult.Error
}
is Fido2ValidateOriginResult.Success -> {
@@ -200,7 +163,16 @@ class Fido2CredentialManagerImpl(
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
origin = Origin.Android(
UnverifiedAssetLink(
callingAppInfo.packageName,
callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error,
origin.toHostOrPathOrNull()
?: return Fido2CredentialAssertionResult.Error,
origin,
),
),
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -212,13 +184,7 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
onFailure = {
Timber.e(it, "Failed to authenticate FIDO2 credential.")
Fido2CredentialAssertionResult.Error(
R.string.passkey_authentication_failed_due_to_an_internal_error
.asText(),
)
},
onFailure = { Fido2CredentialAssertionResult.Error },
)
}
}
@@ -230,13 +196,13 @@ class Fido2CredentialManagerImpl(
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
getPasskeyAssertionOptionsOrNull(requestJson)
?.relyingPartyId
?.prefixHttpsIfNecessaryOrNull()
?.let { "$HTTPS$it" }
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
getPasskeyAttestationOptionsOrNull(requestJson)
?.relyingParty
?.id
?.prefixHttpsIfNecessaryOrNull()
?.let { "$HTTPS$it" }
}
private const val MAX_AUTHENTICATION_ATTEMPTS = 5

View File

@@ -20,4 +20,13 @@ interface Fido2OriginManager {
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult
/**
* Returns the privileged app origin, or null if the calling app is not allowed.
*
* @param callingAppInfo The calling app info.
*
* @return The privileged app origin, or null.
*/
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
}

View File

@@ -32,6 +32,13 @@ class Fido2OriginManagerImpl(
}
}
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
if (!callingAppInfo.isOriginPopulated()) return null
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
?.takeUnless { !callingAppInfo.isOriginPopulated() }
}
private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
@@ -116,10 +123,7 @@ class Fido2OriginManagerImpl(
}
.fold(
onSuccess = { it },
onFailure = {
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
Fido2ValidateOriginResult.Error.Unknown
},
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
)
/**
@@ -153,4 +157,16 @@ class Fido2OriginManagerImpl(
?: false
}
.takeUnless { it.isEmpty() }
private suspend fun getGoogleAllowListOrNull(): String? =
assetManager
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
.getOrNull()
private suspend fun getCommunityAllowListOrNull(): String? =
assetManager
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
.getOrNull()
}

View File

@@ -20,7 +20,6 @@ data class Fido2CreateCredentialRequest(
val packageName: String,
val signingInfo: SigningInfo,
val origin: String?,
val isUserVerified: Boolean?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(

View File

@@ -7,19 +7,6 @@ import kotlinx.parcelize.Parcelize
/**
* Models a FIDO 2 credential authentication request parsed from the launching intent.
*
* @param userId The ID of the Bitwarden user to authenticate.
* @param cipherId The ID of the cipher that contains the passkey to authenticate.
* @param credentialId The ID of the credential to authenticate.
* @param requestJson The JSON representation of the FIDO 2 request.
* @param clientDataHash The hash of the client data.
* @param packageName The package name of the calling app.
* @param signingInfo The signing info of the calling app.
* @param origin The origin of the calling app. Only populated if the calling application is a
* privileged application. I.e., a web browser.
* @param isUserVerified Whether the user has been verified prior to receiving this request. Only
* populated if device biometric verification was performed. If null, the application is responsible
* for prompting user verification when it is deemed necessary.
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
@@ -31,7 +18,6 @@ data class Fido2CredentialAssertionRequest(
val packageName: String,
val signingInfo: SigningInfo,
val origin: String?,
val isUserVerified: Boolean?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(packageName, signingInfo, origin)

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents possible outcomes of a FIDO 2 credential assertion request.
*/
@@ -15,5 +13,5 @@ sealed class Fido2CredentialAssertionResult {
/**
* Indicates there was an error and the assertion was not successful.
*/
data class Error(val message: Text) : Fido2CredentialAssertionResult()
data object Error : Fido2CredentialAssertionResult()
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.model
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents the result of a FIDO 2 Get Credentials request.
@@ -25,7 +24,5 @@ sealed class Fido2GetCredentialsResult {
/**
* Indicates an error was encountered when querying for matching credentials.
*/
data class Error(
val message: Text,
) : Fido2GetCredentialsResult()
data object Error : Fido2GetCredentialsResult()
}

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Models the data returned from creating a FIDO 2 credential.
*/
@@ -11,13 +9,13 @@ sealed class Fido2RegisterCredentialResult {
* Indicates the credential has been successfully registered.
*/
data class Success(
val responseJson: String,
val registrationResponse: String,
) : Fido2RegisterCredentialResult()
/**
* Indicates there was an error and the credential was not registered.
*/
data class Error(val message: Text) : Fido2RegisterCredentialResult()
data object Error : Fido2RegisterCredentialResult()
/**
* Indicates the user cancelled the request.

View File

@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
@SerialName("id")
val id: String,
@SerialName("transports")
val transports: List<String>?,
val transports: List<String>,
)

View File

@@ -6,8 +6,6 @@ import android.os.Build
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialUnsupportedException
import androidx.credentials.exceptions.CreateCredentialCancellationException
@@ -24,7 +22,6 @@ import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.ProviderClearCredentialStateRequest
@@ -37,13 +34,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -52,7 +45,6 @@ import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.launch
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
import javax.crypto.Cipher
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
@@ -62,7 +54,7 @@ const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOU
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
* processing.
*/
@Suppress("LongParameterList", "TooManyFunctions")
@Suppress("LongParameterList")
@RequiresApi(Build.VERSION_CODES.S)
class Fido2ProviderProcessorImpl(
private val context: Context,
@@ -72,8 +64,6 @@ class Fido2ProviderProcessorImpl(
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
private val clock: Clock,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
) : Fido2ProviderProcessor {
@@ -104,6 +94,60 @@ class Fido2ProviderProcessorImpl(
}
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
if (requestJson.isNullOrEmpty()) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.build()
}
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
val accountName = name ?: email
return CreateEntry
.Builder(
accountName = accountName,
pendingIntent = intentManager.createFido2CreationPendingIntent(
CREATE_PASSKEY_INTENT,
userId,
requestCode.getAndIncrement(),
),
)
.setDescription(
context.getString(
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.build()
}
override fun processGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
@@ -158,78 +202,6 @@ class Fido2ProviderProcessorImpl(
}
}
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
if (requestJson.isNullOrEmpty()) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.build()
}
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
accountName = accountName,
pendingIntent = intentManager.createFido2CreationPendingIntent(
CREATE_PASSKEY_INTENT,
userId,
requestCode.getAndIncrement(),
),
)
.setDescription(
context.getString(
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)
if (isVaultUnlocked &&
featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation)
) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
return entryBuilder.build()
}
@Throws(GetCredentialUnsupportedException::class)
private suspend fun getMatchingFido2CredentialEntries(
userId: String,
@@ -289,70 +261,36 @@ class Fido2ProviderProcessorImpl(
): List<CredentialEntry> =
this
.map {
val publicKeyEntryBuilder = PublicKeyCredentialEntry
PublicKeyCredentialEntry
.Builder(
context = context,
username = it.userNameForUi ?: context.getString(R.string.no_username),
pendingIntent = intentManager.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(),
cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(),
),
pendingIntent = intentManager
.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(),
cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(),
),
beginGetPublicKeyCredentialOption = option,
)
.setIcon(
Icon.createWithResource(
context,
R.drawable.ic_bw_passkey,
),
Icon
.createWithResource(
context,
R.drawable.ic_bw_passkey,
),
)
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let {
publicKeyEntryBuilder
.setBiometricPromptDataIfSupported(cipher = it)
}
}
publicKeyEntryBuilder.build()
.build()
}
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): PublicKeyCredentialEntry.Builder {
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
this
} else {
setBiometricPromptData(
biometricPromptData = BiometricPromptData
.Builder()
.buildPromptDataWithCipher(cipher),
)
}
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): CreateEntry.Builder {
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
this
} else {
setBiometricPromptData(
biometricPromptData = BiometricPromptData
.Builder()
.buildPromptDataWithCipher(cipher),
)
}
}
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
private fun BiometricPromptData.Builder.buildPromptDataWithCipher(
cipher: Cipher,
): BiometricPromptData = BiometricPromptData.Builder()
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
.build()
}

View File

@@ -18,7 +18,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
* Checks if this [Intent] contains a [Fido2CreateCredentialRequest] related to an ongoing FIDO 2
* credential creation process.
*/
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
fun Intent.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler
@@ -39,7 +39,6 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
packageName = systemRequest.callingAppInfo.packageName,
signingInfo = systemRequest.callingAppInfo.signingInfo,
origin = systemRequest.callingAppInfo.origin,
isUserVerified = systemRequest.biometricPromptResult?.isSuccessful,
)
}
@@ -68,8 +67,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
val isUserVerified = systemRequest.biometricPromptResult?.isSuccessful
return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId,
@@ -79,7 +76,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
packageName = systemRequest.callingAppInfo.packageName,
signingInfo = systemRequest.callingAppInfo.signingInfo,
origin = systemRequest.callingAppInfo.origin,
isUserVerified = isUserVerified,
)
}

View File

@@ -2,9 +2,6 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -14,31 +11,19 @@ import kotlinx.coroutines.flow.onEach
*/
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
autofillEnabledManager: AutofillEnabledManager,
private val autofillEnabledManager: AutofillEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
get() = autofillManager.isEnabled &&
autofillManager.hasEnabledAutofillServices() &&
autofillManager.isAutofillSupported
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
get() = ChromeThirdPartyAutofillStatus(
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
)
init {
appStateManager
.appForegroundStateFlow
.onEach {
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
chromeAutofillStatus
}
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.launchIn(lifecycleScope)
}
}

View File

@@ -9,7 +9,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
import java.time.Clock
/**
@@ -25,8 +25,15 @@ class AutofillTotpManagerImpl(
) : AutofillTotpManager {
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
if (settingsRepository.isAutoCopyTotpDisabled) return
val organizationPremiumStatusMap = authRepository
.userStateFlow
.value
?.activeAccount
?.getOrganizationPremiumStatusMap()
.orEmpty()
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
if (!isPremium && !cipherView.organizationUseTotp) return
val premiumStatus = organizationPremiumStatusMap[cipherView.organizationId] ?: isPremium
if (!premiumStatus && !cipherView.organizationUseTotp) return
val totpCode = cipherView.login?.totp ?: return
val totpResult = vaultRepository.generateTotp(
@@ -35,10 +42,7 @@ class AutofillTotpManagerImpl(
)
if (totpResult is GenerateTotpResult.Success) {
clipboardManager.setText(
text = totpResult.code,
toastDescriptorOverride = R.string.verification_code_totp.asText(),
)
clipboardManager.setText(text = totpResult.code)
Toast
.makeText(
context.applicationContext,

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager which provides whether specific Chrome versions have third party autofill available and
* enabled.
*/
interface ChromeThirdPartyAutofillEnabledManager {
/**
* Combined status for all concerned Chrome versions.
*/
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
/**
* An observable [StateFlow] of the combined third party autofill status of all concerned
* chrome versions.
*/
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
}

View File

@@ -1,52 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
*/
class ChromeThirdPartyAutofillEnabledManagerImpl(
private val featureFlagManager: FeatureFlagManager,
) : ChromeThirdPartyAutofillEnabledManager {
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
set(value) {
field = value
mutableChromeThirdPartyAutofillStatusStateFlow.update {
value
}
}
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
chromeThirdPartyAutofillStatus,
)
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
get() = mutableChromeThirdPartyAutofillStatusStateFlow
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
) { data, enabled ->
if (enabled) {
data
} else {
DEFAULT_STATUS
}
}
}
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
/**
* Manager class used to determine if a device has installed versions of Chrome (either the
* stable release or beta channel) which support and require opt in to third party autofill.
*/
interface ChromeThirdPartyAutofillManager {
/**
* The data representing the status of the stable chrome version
*/
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
/**
* The data representing the status of the beta chrome version
*/
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
}

View File

@@ -1,62 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
/**
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
* [ContentResolver] to determine if the installed Chrome packages support and enable
* third party autofill services.
*
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
*/
@OmitFromCoverage
class ChromeThirdPartyAutofillManagerImpl(
private val context: Context,
) : ChromeThirdPartyAutofillManager {
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
private fun getThirdPartyAutoFillStatusForChannel(
releaseChannel: ChromeReleaseChannel,
): ChromeThirdPartyAutoFillData {
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
.path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
.build()
val cursor = context
.contentResolver
.query(
/* uri = */ uri,
/* projection = */ arrayOf(THIRD_PARTY_MODE_COLUMN),
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null,
)
var thirdPartyEnabled = false
val isThirdPartyAvailable = cursor
?.let {
it.moveToFirst()
val columnIndex = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
thirdPartyEnabled = it.getInt(columnIndex) != 0
it.close()
true
}
?: false
return ChromeThirdPartyAutoFillData(
isAvailable = isThirdPartyAvailable,
isThirdPartyEnabled = thirdPartyEnabled,
)
}
}

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
/**
* Enumerated values of each version of Chrome supported for third party autofill checks.
*
* @property packageName the package name of the release channel for the Chrome version.
*/
enum class ChromeReleaseChannel(val packageName: String) {
STABLE(CHROME_CHANNEL_PACKAGE),
BETA(BETA_CHANNEL_PACKAGE),
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
/**
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
*/
data class ChromeThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
)
/**
* The overall status for all relevant release channels of Chrome.
*/
data class ChromeThirdPartyAutofillStatus(
val stableStatusData: ChromeThirdPartyAutoFillData,
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
)

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val SERVER_CONFIGURATIONS = "serverConfigurations"

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEv
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
@@ -19,11 +18,6 @@ interface SettingsDiskSource {
*/
var appLanguage: AppLanguage?
/**
* Emits updates that track [AppLanguage].
*/
val appLanguageFlow: Flow<AppLanguage?>
/**
* Has the initial autofill dialog been shown to the user.
*/
@@ -362,44 +356,4 @@ interface SettingsDiskSource {
* Stores the given [count] completed create send actions for the device.
*/
fun storeCreateSendActionCount(count: Int?)
/**
* Gets the Boolean value of if the Add Login CoachMark tour has been interacted with.
*/
fun getShouldShowAddLoginCoachMark(): Boolean?
/**
* Stores a value for if the Add Login CoachMark tour has been interacted with
*/
fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?)
/**
* Returns an [Flow] to observe updates to the "ShouldShowAddLoginCoachMark" value.
*/
fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?>
/**
* Gets the Boolean value of if the Generator CoachMark tour has been interacted with.
*/
fun getShouldShowGeneratorCoachMark(): Boolean?
/**
* Stores a value for if the Generator CoachMark tour has been interacted with
*/
fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?)
/**
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
*/
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
/**
* Stores the given [screenData] as the screen to resume to identified by [userId].
*/
fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?)
/**
* Gets the screen data to resume to for the device identified by [userId] or null if no screen
*/
fun getAppResumeScreen(userId: String): AppResumeScreenData?
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@@ -11,6 +10,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@@ -40,9 +40,6 @@ private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
private const val ADD_ACTION_COUNT = "addActionCount"
private const val COPY_ACTION_COUNT = "copyActionCount"
private const val CREATE_ACTION_COUNT = "createActionCount"
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
private const val RESUME_SCREEN = "resumeScreen"
/**
* Primary implementation of [SettingsDiskSource].
@@ -53,7 +50,6 @@ class SettingsDiskSourceImpl(
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
private val mutableAppThemeFlow = bufferedMutableSharedFlow<AppTheme>(replay = 1)
private val mutableLastSyncFlowMap = mutableMapOf<String, MutableSharedFlow<Instant?>>()
@@ -82,10 +78,6 @@ class SettingsDiskSourceImpl(
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@@ -102,12 +94,8 @@ class SettingsDiskSourceImpl(
key = APP_LANGUAGE_KEY,
value = value?.localeName,
)
mutableAppLanguageFlow.tryEmit(value)
}
override val appLanguageFlow: Flow<AppLanguage?>
get() = mutableAppLanguageFlow.onSubscription { emit(appLanguage) }
override var initialAutofillDialogShown: Boolean?
get() = getBoolean(key = INITIAL_AUTOFILL_DIALOG_SHOWN)
set(value) {
@@ -187,15 +175,12 @@ class SettingsDiskSourceImpl(
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
storeAppResumeScreen(userId = userId, screenData = null)
// The following are intentionally not cleared so they can be
// restored after logging out and back in:
// - screen capture allowed
// - show autofill setting badge
// - show unlock setting badge
// - should show add login coach mark
// - should show generator coach mark
}
override fun getAccountBiometricIntegrityValidity(
@@ -497,48 +482,6 @@ class SettingsDiskSourceImpl(
)
}
override fun getShouldShowAddLoginCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK)
override fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK,
value = shouldShow,
)
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(shouldShow)
}
override fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
emit(getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK))
}
override fun getShouldShowGeneratorCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_GENERATOR_COACH_MARK)
override fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_GENERATOR_COACH_MARK,
value = shouldShow,
)
mutableHasSeenGeneratorCoachMarkFlow.tryEmit(shouldShow)
}
override fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenGeneratorCoachMarkFlow.onSubscription {
emit(getShouldShowGeneratorCoachMark())
}
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
putString(
key = RESUME_SCREEN.appendIdentifier(userId),
value = screenData?.let { json.encodeToString(it) },
)
}
override fun getAppResumeScreen(userId: String): AppResumeScreenData? =
getString(RESUME_SCREEN.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) }
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =

View File

@@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
import java.security.PrivateKey
import java.security.cert.X509Certificate
/**
* Represents a mutual TLS certificate.
*/
data class MutualTlsCertificate(
val alias: String,
val privateKey: PrivateKey,
val certificateChain: List<X509Certificate>,
) {
/**
* Leaf certificate of the chain.
*/
val leafCertificate: X509Certificate?
get() = certificateChain.lastOrNull()
/**
* Root certificate of the chain.
*/
val rootCertificate: X509Certificate?
get() = certificateChain.firstOrNull()
override fun toString(): String = leafCertificate
?.let {
buildString {
appendLine("Subject: ${it.subjectDN}")
appendLine("Issuer: ${it.issuerDN}")
appendLine("Valid From: ${it.notBefore}")
appendLine("Valid Until: ${it.notAfter}")
}
}
?: ""
}

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
/**
* Location of the key data.
*/
enum class MutualTlsKeyHost {
/**
* Key is stored in the system key chain.
*/
KEY_CHAIN,
/**
* Key is stored in a private instance of the Android Key Store.
*/
ANDROID_KEY_STORE,
}

View File

@@ -14,10 +14,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -74,17 +70,6 @@ object PlatformNetworkModule {
@Singleton
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
@Provides
@Singleton
fun provideSslManager(
keyManager: KeyManager,
environmentRepository: EnvironmentRepository,
): SslManager =
SslManagerImpl(
keyManager = keyManager,
environmentRepository = environmentRepository,
)
@Provides
@Singleton
fun provideRetrofits(
@@ -92,7 +77,6 @@ object PlatformNetworkModule {
baseUrlInterceptors: BaseUrlInterceptors,
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
sslManager: SslManager,
json: Json,
): Retrofits =
RetrofitsImpl(
@@ -100,7 +84,6 @@ object PlatformNetworkModule {
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
refreshAuthenticator = refreshAuthenticator,
sslManager = sslManager,
json = json,
)

View File

@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@@ -15,9 +14,6 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import timber.log.Timber
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
/**
* Primary implementation of [Retrofits].
@@ -28,7 +24,6 @@ class RetrofitsImpl(
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
json: Json,
private val sslManager: SslManager,
) : Retrofits {
//region Authenticated Retrofits
@@ -72,10 +67,6 @@ class RetrofitsImpl(
baseClient
.newBuilder()
.addInterceptor(loggingInterceptor)
.setSslSocketFactory(
sslContext = sslManager.sslContext,
trustManagers = sslManager.trustManagers,
)
.build(),
)
.build()
@@ -102,10 +93,6 @@ class RetrofitsImpl(
.newBuilder()
.authenticator(refreshAuthenticator)
.addInterceptor(authTokenInterceptor)
.setSslSocketFactory(
sslContext = sslManager.sslContext,
trustManagers = sslManager.trustManagers,
)
.build()
}
@@ -146,22 +133,9 @@ class RetrofitsImpl(
.newBuilder()
.addInterceptor(baseUrlInterceptor)
.addInterceptor(loggingInterceptor)
.setSslSocketFactory(
sslContext = sslManager.sslContext,
trustManagers = sslManager.trustManagers,
)
.build(),
)
.build()
private fun OkHttpClient.Builder.setSslSocketFactory(
sslContext: SSLContext,
trustManagers: Array<TrustManager>,
): OkHttpClient.Builder =
sslSocketFactory(
sslContext.socketFactory,
trustManagers.first() as X509TrustManager,
)
//endregion Helper properties and functions
}

View File

@@ -15,7 +15,6 @@ import kotlinx.serialization.encoding.Encoder
*/
@Suppress("UnnecessaryAbstractClass")
abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
private val className: String,
private val values: Array<T>,
private val default: T? = null,
) : KSerializer<T> {
@@ -30,7 +29,7 @@ abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
val decodedValue = decoder.decodeInt().toString()
return values.firstOrNull { it.serialNameAnnotation?.value == decodedValue }
?: default
?: throw IllegalArgumentException("Unknown value $decodedValue for $className")
?: throw IllegalArgumentException("Unknown value $decodedValue")
}
override fun serialize(encoder: Encoder, value: T) {

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
/**
* Interface for managing SSL connections.
*/
interface SslManager {
/**
* The SSL context to use for SSL connections.
*/
val sslContext: SSLContext
/**
* The trust managers to use for SSL connections.
*/
val trustManagers: Array<TrustManager>
}

View File

@@ -1,116 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.security.PrivateKey
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
/**
* Primary implementation of [SslManager].
*/
class SslManagerImpl(
private val keyManager: KeyManager,
private val environmentRepository: EnvironmentRepository,
) : SslManager {
/*
This property must only be accessed from a background thread. Accessing this property from
the main thread will result in an exception being thrown when retrieving the mutual TLS
certificate from [KeyManager].
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@get:WorkerThread
internal val mutualTlsCertificate: MutualTlsCertificate?
get() {
val keyUri = getKeyUri()
?: return null
val host = MutualTlsKeyHost
.entries
.find { it.name == keyUri.authority }
?: return null
val alias = keyUri.path
?.trim('/')
?.takeUnless { it.isEmpty() }
?: return null
return keyManager.getMutualTlsCertificateChain(
alias = alias,
host = host,
)
}
override val trustManagers: Array<TrustManager>
get() = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
.apply { init(null as KeyStore?) }
.trustManagers
override val sslContext: SSLContext
get() = SSLContext
.getInstance("TLS")
.apply {
init(
arrayOf(X509ExtendedKeyManagerImpl()),
trustManagers,
null,
)
}
private fun getKeyUri(): Uri? = environmentRepository
.environment
.environmentUrlData
.keyUri
?.toUri()
private inner class X509ExtendedKeyManagerImpl : X509ExtendedKeyManager() {
override fun chooseClientAlias(
keyType: Array<out String>?,
issuers: Array<out Principal>?,
socket: Socket?,
): String = mutualTlsCertificate?.alias ?: ""
override fun getCertificateChain(
alias: String?,
): Array<X509Certificate>? =
mutualTlsCertificate
?.certificateChain
?.toTypedArray()
override fun getPrivateKey(alias: String?): PrivateKey? =
mutualTlsCertificate
?.privateKey
//region Unused server side methods
override fun getServerAliases(
alias: String?,
issuers: Array<out Principal>?,
): Array<String> = arrayOf()
override fun getClientAliases(
keyType: String?,
issuers: Array<out Principal>?,
): Array<String> = emptyArray()
override fun chooseServerAlias(
alias: String?,
issuers: Array<out Principal>?,
socket: Socket?,
): String = ""
//endregion Unused server side methods
}
}

View File

@@ -1,37 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
/**
* Manages the screen from which the app should be resumed after unlock.
*/
interface AppResumeManager {
/**
* Sets the screen from which the app should be resumed after unlock.
*
* @param screenData The screen identifier (e.g., "HomeScreen", "SettingsScreen").
*/
fun setResumeScreen(screenData: AppResumeScreenData)
/**
* Gets the screen from which the app should be resumed after unlock.
*
* @return The screen identifier, or an empty string if not set.
*/
fun getResumeScreen(): AppResumeScreenData?
/**
* Gets the special circumstance associated with the resume screen for the current user.
*
* @return The special circumstance, or null if no special circumstance
* is associated with the resume screen.
*/
fun getResumeSpecialCircumstance(): SpecialCircumstance?
/**
* Clears the saved resume screen for the current user.
*/
fun clearResumeScreen()
}

View File

@@ -1,74 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import java.time.Clock
private const val UNLOCK_NAVIGATION_TIME_SECONDS: Long = 5 * 60
/**
* Primary implementation of [AppResumeManager].
*/
class AppResumeManagerImpl(
private val settingsDiskSource: SettingsDiskSource,
private val authDiskSource: AuthDiskSource,
private val authRepository: AuthRepository,
private val vaultLockManager: VaultLockManager,
private val clock: Clock,
) : AppResumeManager {
override fun setResumeScreen(screenData: AppResumeScreenData) {
authRepository.activeUserId?.let {
settingsDiskSource.storeAppResumeScreen(
userId = it,
screenData = screenData,
)
}
}
override fun getResumeScreen(): AppResumeScreenData? {
return authRepository.activeUserId?.let { userId ->
settingsDiskSource.getAppResumeScreen(userId)
}
}
override fun getResumeSpecialCircumstance(): SpecialCircumstance? {
val userId = authRepository.activeUserId ?: return null
val timeNowMinus5Min = clock.instant().minusSeconds(UNLOCK_NAVIGATION_TIME_SECONDS)
val lastLockTimestamp = authDiskSource
.getLastLockTimestamp(userId = userId)
?: return null
if (timeNowMinus5Min.isAfter(lastLockTimestamp)) {
settingsDiskSource.storeAppResumeScreen(userId = userId, screenData = null)
return null
}
return when (val resumeScreenData = getResumeScreen()) {
AppResumeScreenData.GeneratorScreen -> SpecialCircumstance.GeneratorShortcut
AppResumeScreenData.SendScreen -> SpecialCircumstance.SendShortcut
is AppResumeScreenData.SearchScreen -> SpecialCircumstance.SearchShortcut(
searchTerm = resumeScreenData.searchTerm,
)
AppResumeScreenData.VerificationCodeScreen -> {
SpecialCircumstance.VerificationCodeShortcut
}
else -> null
}
}
override fun clearResumeScreen() {
val userId = authRepository.activeUserId ?: return
if (vaultLockManager.isVaultUnlocked(userId = userId)) {
settingsDiskSource.storeAppResumeScreen(
userId = userId,
screenData = null,
)
}
}
}

View File

@@ -13,11 +13,6 @@ interface BiometricsEncryptionManager {
userId: String,
): Cipher?
/**
* Clears the data associated with the users biometrics.
*/
fun clearBiometrics(userId: String)
/**
* Gets the [Cipher] built from a keystore, or creates one if it doesn't already exist.
*/

View File

@@ -26,7 +26,6 @@ import javax.crypto.spec.IvParameterSpec
* Default implementation of [BiometricsEncryptionManager] for managing Android keystore encryption
* and decryption.
*/
@Suppress("TooManyFunctions")
@OmitFromCoverage
class BiometricsEncryptionManagerImpl(
private val authDiskSource: AuthDiskSource,
@@ -36,8 +35,20 @@ class BiometricsEncryptionManagerImpl(
.getInstance(ENCRYPTION_KEYSTORE_NAME)
.also { it.load(null) }
private val keyGenParameterSpec: KeyGenParameterSpec
get() = KeyGenParameterSpec
.Builder(
ENCRYPTION_KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
override fun createCipherOrNull(userId: String): Cipher? {
val secretKey: SecretKey = generateKeyOrNull(userId = userId)
val secretKey: SecretKey = generateKeyOrNull()
?: run {
// user removed all biometrics from the device
destroyBiometrics(userId = userId)
@@ -57,25 +68,9 @@ class BiometricsEncryptionManagerImpl(
return cipher
}
override fun clearBiometrics(userId: String) {
settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState ->
settingsDiskSource.storeAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBioIntegrityState,
value = null,
)
}
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
keystore.deleteEntry(encryptionKeyName(userId = userId))
}
override fun getOrCreateCipher(userId: String): Cipher? {
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
// is available. If neither succeeds, then we need to generate a new one.
val secretKey: SecretKey = getSecretKeyOrNull(userId = userId)
?: getSecretKeyOrNull(userId = null)
?: generateKeyOrNull(userId = userId)
val secretKey: SecretKey = getSecretKeyOrNull()
?: generateKeyOrNull()
?: run {
// user removed all biometrics from the device
destroyBiometrics(userId = userId)
@@ -102,26 +97,11 @@ class BiometricsEncryptionManagerImpl(
?: false
}
private fun getKeyGenParameterSpec(userId: String): KeyGenParameterSpec =
KeyGenParameterSpec
.Builder(
encryptionKeyName(userId = userId),
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
private fun encryptionKeyName(userId: String?): String =
"${BuildConfig.APPLICATION_ID}.biometric_integrity${userId?.let { ".$it" }.orEmpty()}"
/**
* Generates a [SecretKey] from which the [Cipher] will be generated, or `null` if a key cannot
* be generated.
*/
private fun generateKeyOrNull(userId: String): SecretKey? {
private fun generateKeyOrNull(): SecretKey? {
val keyGen = try {
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME)
} catch (_: NoSuchAlgorithmException) {
@@ -133,7 +113,7 @@ class BiometricsEncryptionManagerImpl(
}
return try {
keyGen.init(getKeyGenParameterSpec(userId = userId))
keyGen.init(keyGenParameterSpec)
keyGen.generateKey()
} catch (_: InvalidAlgorithmParameterException) {
null
@@ -145,10 +125,10 @@ class BiometricsEncryptionManagerImpl(
/**
* Returns the [SecretKey] stored in the keystore, or null if there isn't one.
*/
private fun getSecretKeyOrNull(userId: String?): SecretKey? =
private fun getSecretKeyOrNull(): SecretKey? =
try {
keystore
.getKey(encryptionKeyName(userId = userId), null)
.getKey(ENCRYPTION_KEY_NAME, null)
?.let { it as SecretKey }
} catch (_: KeyStoreException) {
// keystore was not loaded
@@ -192,9 +172,7 @@ class BiometricsEncryptionManagerImpl(
* Validates the keystore key and decrypts it using the user-provided [cipher].
*/
private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean {
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
// is available.
val secretKey = getSecretKeyOrNull(userId = userId) ?: getSecretKeyOrNull(userId = null)
val secretKey = getSecretKeyOrNull()
return if (cipher != null && secretKey != null) {
cipher.initializeCipher(userId = userId, secretKey = secretKey)
} else {
@@ -219,12 +197,22 @@ class BiometricsEncryptionManagerImpl(
}
private fun destroyBiometrics(userId: String) {
clearBiometrics(userId = userId)
settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState ->
settingsDiskSource.storeAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBioIntegrityState,
value = null,
)
}
settingsDiskSource.systemBiometricIntegritySource = null
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
keystore.deleteEntry(ENCRYPTION_KEY_NAME)
}
}
private const val ENCRYPTION_KEYSTORE_NAME: String = "AndroidKeyStore"
private const val ENCRYPTION_KEY_NAME: String = "${BuildConfig.APPLICATION_ID}.biometric_integrity"
private const val CIPHER_TRANSFORMATION =
KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -40,17 +39,6 @@ interface FirstTimeActionManager {
*/
val firstTimeStateFlow: Flow<FirstTimeState>
/**
* Returns observable flow of if a user on the device has seen the Add Login coach mark tour.
*/
val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
/**
* Returns observable flow of if a user on the device has seen the Generator screen
* coach mark tour.
*/
val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
@@ -78,9 +66,4 @@ interface FirstTimeActionManager {
* Update the value of the showImportLoginsSettingsBadge status for the active user.
*/
fun storeShowImportLoginsSettingsBadge(showBadge: Boolean)
/**
* Can be called to indicate that a user has seen the AddLogin coach mark tour.
*/
fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType)
}

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -155,32 +154,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
}
.distinctUntilChanged()
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowAddLoginCoachMarkFlow()
.map { it ?: true }
.mapFalseIfAnyLoginCiphersAvailable()
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { shouldShow, featureIsEnabled ->
// If the feature flag is off always return true so observers know
// the card has not been shown.
shouldShow && featureIsEnabled
}
.distinctUntilChanged()
override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowGeneratorCoachMarkFlow()
.map { it ?: true }
.mapFalseIfAnyLoginCiphersAvailable()
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { shouldShow, featureFlagEnabled ->
shouldShow && featureFlagEnabled
}
.distinctUntilChanged()
/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
@@ -238,18 +211,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
)
}
override fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType) {
when (tourCompleted) {
CoachMarkTourType.ADD_LOGIN -> {
settingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = false)
}
CoachMarkTourType.GENERATOR -> {
settingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
}
}
}
/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
@@ -296,23 +257,4 @@ class FirstTimeActionManagerImpl @Inject constructor(
return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false &&
!autofillEnabledManager.isAutofillEnabled
}
/**
* If there are any existing "Login" type ciphers then we'll map the current value
* of the receiver Flow to `false`.
*/
@OptIn(ExperimentalCoroutinesApi::class)
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest { activeUserId ->
combine(
flow = this,
flow2 = vaultDiskSource.getCiphers(activeUserId),
) { currentValue, ciphers ->
currentValue && ciphers.none { it.login != null }
}
}
.distinctUntilChanged()
}

View File

@@ -1,41 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import androidx.annotation.WorkerThread
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
/**
* Primary access point for disk information related to key data.
*/
interface KeyManager {
/**
* Import a private key into the application KeyStore.
*
* @param key The private key to be saved.
* @param alias Alias to be assigned to the private key.
* @param password Password used to protect the certificate.
*/
fun importMutualTlsCertificate(
key: ByteArray,
alias: String,
password: String,
): ImportPrivateKeyResult
/**
* Removes the mTLS key from storage.
*/
fun removeMutualTlsKey(alias: String, host: MutualTlsKeyHost)
/**
* Retrieve the certificate chain for the selected mTLS key.
*
* Must be called from a background thread to prevent possible deadlocks on the main thread.
*/
@WorkerThread
fun getMutualTlsCertificateChain(
alias: String,
host: MutualTlsKeyHost,
): MutualTlsCertificate?
}

View File

@@ -1,188 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import android.content.Context
import android.security.KeyChain
import android.security.KeyChainException
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
import timber.log.Timber
import java.io.IOException
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import java.security.UnrecoverableKeyException
import java.security.cert.Certificate
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
/**
* Default implementation of [KeyManager].
*/
class KeyManagerImpl(
private val context: Context,
) : KeyManager {
@Suppress("CyclomaticComplexMethod")
override fun importMutualTlsCertificate(
key: ByteArray,
alias: String,
password: String,
): ImportPrivateKeyResult {
// Step 1: Load PKCS12 bytes into a KeyStore.
val pkcs12KeyStore: KeyStore = key
.inputStream()
.use { stream ->
try {
KeyStore.getInstance(KEYSTORE_TYPE_PKCS12)
.also { it.load(stream, password.toCharArray()) }
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to load PKCS12 bytes")
return ImportPrivateKeyResult.Error.UnsupportedKey
} catch (e: IOException) {
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
return when (e.cause) {
is UnrecoverableKeyException -> {
ImportPrivateKeyResult.Error.UnrecoverableKey
}
else -> {
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
}
}
} catch (e: CertificateException) {
Timber.Forest.e(e, "Unable to load certificate chain")
return ImportPrivateKeyResult.Error.InvalidCertificateChain
} catch (e: NoSuchAlgorithmException) {
Timber.Forest.e(e, "Cryptographic algorithm not supported")
return ImportPrivateKeyResult.Error.UnsupportedKey
}
}
// Step 2: Get a list of aliases and choose the first one.
val internalAlias = pkcs12KeyStore.aliases()
?.takeIf { it.hasMoreElements() }
?.nextElement()
?: return ImportPrivateKeyResult.Error.UnsupportedKey
// Step 3: Extract PrivateKey and X.509 certificate from the KeyStore and verify
// certificate alias.
val privateKey = try {
pkcs12KeyStore.getKey(internalAlias, password.toCharArray())
?: return ImportPrivateKeyResult.Error.UnrecoverableKey
} catch (e: UnrecoverableKeyException) {
Timber.Forest.e(e, "Failed to get private key")
return ImportPrivateKeyResult.Error.UnrecoverableKey
}
val certChain: Array<Certificate> = pkcs12KeyStore
.getCertificateChain(internalAlias)
?.takeUnless { it.isEmpty() }
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
// does not exists.
with(androidKeyStore) {
if (containsAlias(alias)) {
return ImportPrivateKeyResult.Error.DuplicateAlias
}
try {
setKeyEntry(alias, privateKey, null, certChain)
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to import key into Android KeyStore")
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed
}
}
return ImportPrivateKeyResult.Success(alias)
}
override fun removeMutualTlsKey(
alias: String,
host: MutualTlsKeyHost,
) {
when (host) {
MutualTlsKeyHost.ANDROID_KEY_STORE -> removeKeyFromAndroidKeyStore(alias)
else -> Unit
}
}
override fun getMutualTlsCertificateChain(
alias: String,
host: MutualTlsKeyHost,
): MutualTlsCertificate? = when (host) {
MutualTlsKeyHost.ANDROID_KEY_STORE -> getKeyFromAndroidKeyStore(alias)
MutualTlsKeyHost.KEY_CHAIN -> getSystemKeySpecOrNull(alias)
}
private fun removeKeyFromAndroidKeyStore(alias: String) {
try {
androidKeyStore.deleteEntry(alias)
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to remove key from Android KeyStore")
}
}
private fun getSystemKeySpecOrNull(alias: String): MutualTlsCertificate? {
val systemPrivateKey = try {
KeyChain.getPrivateKey(context, alias)
} catch (e: KeyChainException) {
Timber.Forest.e(e, "Requested alias not found in system KeyChain")
null
}
?: return null
val systemCertificateChain = try {
KeyChain.getCertificateChain(context, alias)
} catch (e: KeyChainException) {
Timber.Forest.e(e, "Unable to access certificate chain for provided alias")
null
}
?: return null
return MutualTlsCertificate(
alias = alias,
certificateChain = systemCertificateChain.toList(),
privateKey = systemPrivateKey,
)
}
private fun getKeyFromAndroidKeyStore(alias: String): MutualTlsCertificate? =
with(androidKeyStore) {
try {
val privateKeyRef = (getKey(alias, null) as? PrivateKey)
?: return null
val certChain = getCertificateChain(alias)
.mapNotNull { it as? X509Certificate }
.takeUnless { it.isEmpty() }
?: return null
MutualTlsCertificate(
alias = alias,
certificateChain = certChain,
privateKey = privateKeyRef,
)
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to load Android KeyStore")
null
} catch (e: UnrecoverableKeyException) {
Timber.Forest.e(e, "Failed to load client certificate from Android KeyStore")
null
} catch (e: NoSuchAlgorithmException) {
Timber.Forest.e(e, "Key cannot be recovered. Password may be incorrect.")
null
} catch (e: NoSuchAlgorithmException) {
Timber.Forest.e(e, "Algorithm not supported")
null
}
}
private val androidKeyStore
get() = KeyStore
.getInstance(KEYSTORE_TYPE_ANDROID)
.also { it.load(null) }
}
private const val KEYSTORE_TYPE_ANDROID = "AndroidKeyStore"
private const val KEYSTORE_TYPE_PKCS12 = "pkcs12"

View File

@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.platform.manager.network
package com.x8bit.bitwarden.data.platform.manager
/**
* Responsible for managing the active configuration of the network layer.

View File

@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.platform.manager.network
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator

View File

@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.platform.manager.network
package com.x8bit.bitwarden.data.platform.manager
/**
* Manager to detect and handle changes to network connectivity.

View File

@@ -1,4 +1,4 @@
package com.x8bit.bitwarden.data.platform.manager.network
package com.x8bit.bitwarden.data.platform.manager
import android.content.Context
import android.net.ConnectivityManager

View File

@@ -93,21 +93,13 @@ class PolicyManagerImpl(
organization: SyncResponseJson.Profile.Organization,
policyType: PolicyTypeJson,
): Boolean =
when (policyType) {
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
organization.type == OrganizationType.OWNER
}
PolicyTypeJson.PASSWORD_GENERATOR,
PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
-> {
false
}
else -> {
(organization.type == OrganizationType.OWNER ||
organization.type == OrganizationType.ADMIN) ||
organization.permissions.shouldManagePolicies
}
if (policyType == PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) {
organization.type == OrganizationType.OWNER
} else if (policyType == PolicyTypeJson.PASSWORD_GENERATOR) {
false
} else {
(organization.type == OrganizationType.OWNER ||
organization.type == OrganizationType.ADMIN) ||
organization.permissions.shouldManagePolicies
}
}

View File

@@ -31,15 +31,6 @@ interface BitwardenClipboardManager {
toastDescriptorOverride: String? = null,
)
/**
* See [setText] for more details.
*/
fun setText(
text: String,
isSensitive: Boolean = true,
toastDescriptorOverride: Text,
)
/**
* See [setText] for more details.
*/

View File

@@ -48,10 +48,7 @@ class BitwardenClipboardManagerImpl(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
val descriptor = toastDescriptorOverride
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
?: context.resources.getString(
R.string.value_has_been_copied,
context.resources.getString(R.string.value),
)
?: context.resources.getString(R.string.copied_to_clipboard)
Toast.makeText(context, descriptor, Toast.LENGTH_SHORT).show()
}
@@ -73,14 +70,6 @@ class BitwardenClipboardManagerImpl(
setText(text.toAnnotatedString(), isSensitive, toastDescriptorOverride)
}
override fun setText(text: String, isSensitive: Boolean, toastDescriptorOverride: Text) {
setText(
text.toAnnotatedString(),
isSensitive,
toastDescriptorOverride.toString(context.resources),
)
}
override fun setText(text: Text, isSensitive: Boolean, toastDescriptorOverride: String?) {
setText(text.toString(context.resources), isSensitive, toastDescriptorOverride)
}

View File

@@ -15,8 +15,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterM
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.AppResumeManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
@@ -30,10 +28,12 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -54,10 +54,6 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
@@ -68,7 +64,6 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
@@ -334,28 +329,4 @@ object PlatformManagerModule {
autofillEnabledManager = autofillEnabledManager,
accessibilityEnabledManager = accessibilityEnabledManager,
)
@Provides
@Singleton
fun provideKeyManager(
@ApplicationContext context: Context,
): KeyManager = KeyManagerImpl(context = context)
@Provides
@Singleton
fun provideAppResumeManager(
settingsDiskSource: SettingsDiskSource,
authDiskSource: AuthDiskSource,
authRepository: AuthRepository,
vaultLockManager: VaultLockManager,
clock: Clock,
): AppResumeManager {
return AppResumeManagerImpl(
settingsDiskSource = settingsDiskSource,
authDiskSource = authDiskSource,
authRepository = authRepository,
vaultLockManager = vaultLockManager,
clock = clock,
)
}
}

View File

@@ -1,34 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
import kotlinx.serialization.Serializable
/**
* Data class representing the Screen Data for app resume.
*/
@Serializable
sealed class AppResumeScreenData {
/**
* Data object representing the Generator screen for app resume.
*/
@Serializable
data object GeneratorScreen : AppResumeScreenData()
/**
* Data object representing the Send screen for app resume.
*/
@Serializable
data object SendScreen : AppResumeScreenData()
/**
* Data class representing the Search screen for app resume.
*/
@Serializable
data class SearchScreen(val searchTerm: String) : AppResumeScreenData()
/**
* Data object representing the Verification Code screen for app resume.
*/
@Serializable
data object VerificationCodeScreen : AppResumeScreenData()
}

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Enumerated values to represent all the possible coach mark tours that can be
* completed.
*
* @see com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
*/
enum class CoachMarkTourType {
ADD_LOGIN,
GENERATOR,
}

View File

@@ -39,12 +39,6 @@ sealed class FlagKey<out T : Any> {
NewDevicePermanentDismiss,
NewDeviceTemporaryDismiss,
IgnoreEnvironmentCheck,
MutualTls,
SingleTapPasskeyCreation,
SingleTapPasskeyAuthentication,
AnonAddySelfHostAlias,
SimpleLoginSelfHostAlias,
ChromeAutofill,
)
}
}
@@ -53,7 +47,7 @@ sealed class FlagKey<out T : Any> {
* Data object holding the key for syncing with the Bitwarden Authenticator app.
*/
data object AuthenticatorSync : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-bwa-sync"
override val keyName: String = "enable-authenticator-sync-android"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
@@ -73,7 +67,7 @@ sealed class FlagKey<out T : Any> {
data object OnboardingCarousel : FlagKey<Boolean>() {
override val keyName: String = "native-carousel-flow"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
override val isRemotelyConfigured: Boolean = false
}
/**
@@ -82,7 +76,7 @@ sealed class FlagKey<out T : Any> {
data object OnboardingFlow : FlagKey<Boolean>() {
override val keyName: String = "native-create-account-flow"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
override val isRemotelyConfigured: Boolean = false
}
/**
@@ -91,7 +85,7 @@ sealed class FlagKey<out T : Any> {
data object ImportLoginsFlow : FlagKey<Boolean>() {
override val keyName: String = "import-logins-flow"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
override val isRemotelyConfigured: Boolean = false
}
/**
@@ -146,7 +140,7 @@ sealed class FlagKey<out T : Any> {
*/
data object CipherKeyEncryption : FlagKey<Boolean>() {
override val keyName: String = "cipher-key-encryption"
override val defaultValue: Boolean = false
override val defaultValue: Boolean = true
override val isRemotelyConfigured: Boolean = true
}
@@ -177,62 +171,6 @@ sealed class FlagKey<out T : Any> {
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the feature flag key for the Mutual TLS feature.
*/
data object MutualTls : FlagKey<Boolean>() {
override val keyName: String = "mutual-tls"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to enable single tap passkey creation.
*/
data object SingleTapPasskeyCreation : FlagKey<Boolean>() {
override val keyName: String = "single-tap-passkey-creation"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to enable single tap passkey authentication.
*/
data object SingleTapPasskeyAuthentication : FlagKey<Boolean>() {
override val keyName: String = "single-tap-passkey-authentication"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to enable AnonAddy (addy.io) self host alias
* generation.
*/
data object AnonAddySelfHostAlias : FlagKey<Boolean>() {
override val keyName: String = "anon-addy-self-host-alias"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to enable SimpleLogin self-host alias generation.
*/
data object SimpleLoginSelfHostAlias : FlagKey<Boolean>() {
override val keyName: String = "simple-login-self-host-alias"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to enable the checking for Chrome's third party
* autofill.
*/
data object ChromeAutofill : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-chrome-autofill"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
//region Dummy keys for testing
/**
* Data object holding the key for a [Boolean] flag to be used in tests.

View File

@@ -1,45 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Models the result of importing a private key.
*/
sealed class ImportPrivateKeyResult {
/**
* Represents a successful result of importing a private key.
*
* @property alias The alias assigned to the imported private key.
*/
data class Success(val alias: String) : ImportPrivateKeyResult()
/**
* Represents a generic error during the import process.
*/
sealed class Error : ImportPrivateKeyResult() {
/**
* Indicates that the provided key is unrecoverable or the password is incorrect.
*/
data object UnrecoverableKey : Error()
/**
* Indicates that the certificate chain associated with the key is invalid.
*/
data object InvalidCertificateChain : Error()
/**
* Indicates that the specified alias is already in use.
*/
data object DuplicateAlias : Error()
/**
* Indicates that an error occurred during the key store operation.
*/
data object KeyStoreOperationFailed : Error()
/**
* Indicates the provided key is not supported.
*/
data object UnsupportedKey : Error()
}
}

View File

@@ -63,7 +63,5 @@ enum class NotificationType {
}
@Keep
private class NotificationTypeSerializer : BaseEnumeratedIntSerializer<NotificationType>(
className = "NotificationType",
values = NotificationType.entries.toTypedArray(),
)
private class NotificationTypeSerializer :
BaseEnumeratedIntSerializer<NotificationType>(NotificationType.entries.toTypedArray())

View File

@@ -130,6 +130,5 @@ enum class OrganizationEventType {
@Keep
private class OrganizationEventTypeSerializer : BaseEnumeratedIntSerializer<OrganizationEventType>(
className = "OrganizationEventType",
values = OrganizationEventType.entries.toTypedArray(),
)

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