Compare commits

..

2 Commits

Author SHA1 Message Date
David Perez
b497156302 🍒 PM-25143: Retain intent data on recreate (#5798) 2025-08-27 20:15:00 +00:00
Patrick Honkonen
ab90f5ff95 🍒[PM-25057] Refactor card restriction logic in AutofillCipherProvider (#5791) 2025-08-27 14:43:23 +00:00
808 changed files with 23520 additions and 44875 deletions

View File

@@ -1,105 +0,0 @@
# Claude Guidelines
Core directives for maintaining code quality and consistency in the Bitwarden Android project.
## Core Directives
**You MUST follow these directives at all times.**
1. **Adhere to Architecture**: All code modifications MUST follow patterns in `docs/ARCHITECTURE.md`
2. **Follow Code Style**: ALWAYS follow `docs/STYLE_AND_BEST_PRACTICES.md`
3. **Error Handling**: Use Result types and sealed classes per architecture guidelines
4. **Best Practices**: Follow Kotlin idioms (immutability, appropriate data structures, coroutines)
5. **Document Everything**: All public APIs require KDoc documentation
6. **Dependency Management**: Use Hilt DI patterns as established in the project
7. **Use Established Patterns**: Leverage existing components before creating new ones
8. **File References**: Use file:line_number format when referencing code
## Code Quality Standards
### Module Organization
**Core Library Modules:**
- **`:core`** - Common utilities and managers shared across multiple modules
- **`:data`** - Data sources, database, data repositories
- **`:network`** - Networking interfaces, API clients, network utilities
- **`:ui`** - Reusable Bitwarden Composables, theming, UI utilities
**Application Modules:**
- **`:app`** - Password Manager application, feature screens, ViewModels, DI setup
- **`:authenticator`** - Authenticator application for 2FA/TOTP code generation
**Specialized Library Modules:**
- **`:authenticatorbridge`** - Communication bridge between :authenticator and :app
- **`:annotation`** - Custom annotations for code generation (Hilt, Room, etc.)
- **`:cxf`** - Android Credential Exchange (CXF/CXP) integration layer
### Patterns Enforcement
- **MVVM + UDF**: ViewModels with StateFlow, Compose UI
- **Hilt DI**: Interface injection, @HiltViewModel, @Inject constructor
- **Testing**: JUnit 5, MockK, Turbine for Flow testing
- **Error Handling**: Sealed Result types, no throws in business logic
## Security Requirements
**Every change must consider:**
- Zero-knowledge architecture preservation
- Proper encryption key handling (Android Keystore)
- Input validation and sanitization
- Secure data storage patterns
- Threat model implications
## Workflow Practices
### Before Implementation
1. Read relevant architecture documentation
2. Search for existing patterns to follow
3. Identify affected modules and dependencies
4. Consider security implications
### During Implementation
1. Follow existing code style in surrounding files
2. Write tests alongside implementation
3. Add KDoc to all public APIs
4. Validate against architecture guidelines
### After Implementation
1. Ensure all tests pass
2. Verify compilation succeeds
3. Review security considerations
4. Update relevant documentation
## Anti-Patterns
**Avoid these:**
- Creating new patterns when established ones exist
- Exception-based error handling in business logic
- Direct dependency access (use DI)
- Mutable state in ViewModels (use StateFlow)
- Missing null safety handling
- Undocumented public APIs
- Tight coupling between modules
## Communication & Decision-Making
Always clarify ambiguous requirements before implementing. Use specific questions:
- "Should this use [Approach A] or [Approach B]?"
- "This affects [X]. Proceed or review first?"
- "Expected behavior for [specific requirement]?"
Defer high-impact decisions to the user:
- Architecture/module changes, public API modifications
- Security mechanisms, database migrations
- Third-party library additions
## Reference Documentation
Critical resources:
- `docs/ARCHITECTURE.md` - Architecture patterns and principles
- `docs/STYLE_AND_BEST_PRACTICES.md` - Code style guidelines
**Do not duplicate information from these files - reference them instead.**

View File

@@ -1,20 +0,0 @@
Use the `reviewing-changes` skill to review this pull request.
The PR branch is already checked out in the current working directory.
Provide a comprehensive review including:
- Summary of changes since last review
- Critical issues found (be thorough)
- Suggested improvements (be thorough)
- Good practices observed (be concise - list only the most notable items without elaboration)
- Action items for the author
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets
When reviewing subsequent commits:
- Track status of previously identified issues (fixed/unfixed/reopened)
- Identify NEW problems introduced since last review
- Note if fixes introduced new issues
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.

View File

@@ -1,110 +0,0 @@
---
name: reviewing-changes
description: Performs comprehensive code reviews for Bitwarden Android projects, verifying architecture compliance, style guidelines, compilation safety, test coverage, and security requirements. Use when reviewing pull requests, checking commits, analyzing code changes, verifying Bitwarden coding standards, evaluating MVVM patterns, checking Hilt DI usage, reviewing security implementations, or assessing test coverage. Automatically invoked by CI pipeline or manually for interactive code reviews.
---
# Reviewing Changes
## Instructions
Follow this process to review code changes for Bitwarden Android:
### Step 1: Understand Context
Start with high-level assessment of the change's purpose and approach. Read PR/commit descriptions and understand what problem is being solved.
### Step 2: Verify Compliance
Systematically check each area against Bitwarden standards documented in `CLAUDE.md`:
1. **Architecture**: Follow patterns in `docs/ARCHITECTURE.md`
- MVVM + UDF (ViewModels with `StateFlow`, Compose UI)
- Hilt DI (interface injection, `@HiltViewModel`)
- Repository pattern and proper data flow
2. **Style**: Adhere to `docs/STYLE_AND_BEST_PRACTICES.md`
- Naming conventions, code organization, formatting
- Kotlin idioms (immutability, null safety, coroutines)
3. **Compilation**: Analyze for potential build issues
- Import statements and dependencies
- Type safety and null safety
- API compatibility and deprecation warnings
- Resource references and manifest requirements
4. **Testing**: Verify appropriate test coverage
- Unit tests for business logic and utility functions
- Integration tests for complex workflows
- UI tests for user-facing features when applicable
- Test coverage for edge cases and error scenarios
5. **Security**: Given Bitwarden's security-focused nature
- Proper handling of sensitive data
- Secure storage practices (Android Keystore)
- Authentication and authorization patterns
- Data encryption and decryption flows
- Zero-knowledge architecture preservation
### Step 3: Document Findings
Identify specific violations with `file:line_number` references. Be precise about locations.
### Step 4: Provide Recommendations
Give actionable recommendations for improvements. Explain why changes are needed and suggest specific solutions.
### Step 5: Flag Critical Issues
Highlight issues that must be addressed before merge. Distinguish between blockers and suggestions.
### Step 6: Acknowledge Quality
Note well-implemented patterns (briefly, without elaboration). Keep positive feedback concise.
## Review Anti-Patterns (DO NOT)
- Be nitpicky about linter-catchable style issues
- Review without understanding context - ask for clarification first
- Focus only on new code - check surrounding context for issues
- Request changes outside the scope of this changeset
## Examples
### Good Review Format
```markdown
## Summary
This PR adds biometric authentication to the login flow, implementing MVVM pattern with proper state management.
## Critical Issues
- `app/login/LoginViewModel.kt:45` - Mutable state exposed; use `StateFlow` instead of `MutableStateFlow`
- `data/auth/BiometricRepository.kt:120` - Missing null safety check on `biometricPrompt` result
## Suggested Improvements
- Consider extracting biometric prompt logic to separate use case class
- Add integration tests for biometric failure scenarios
- `app/login/LoginScreen.kt:89` - Consider using existing `BitwardenButton` component
## Good Practices
- Proper Hilt DI usage throughout
- Comprehensive unit test coverage
- Clear separation of concerns
## Action Items
1. Fix mutable state exposure in `LoginViewModel`
2. Add null safety check in `BiometricRepository`
3. Consider adding integration tests for error flows
```
### What to Focus On
**DO focus on:**
- Architecture violations (incorrect patterns)
- Security issues (data handling, encryption)
- Missing tests for critical paths
- Compilation risks (type safety, null safety)
**DON'T focus on:**
- Minor formatting (handled by linters)
- Personal preferences without architectural basis
- Issues outside the changeset scope

11
.github/CODEOWNERS vendored
View File

@@ -10,11 +10,6 @@
# Actions and workflow changes.
.github/ @bitwarden/dept-development-mobile
# Claude related files
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme
# Auth
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev
# app/src/main/java/com/x8bit/bitwarden/ui/auth @bitwarden/team-auth-dev
@@ -53,9 +48,3 @@
# app/src/main/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
# app/src/test/java/com/x8bit/bitwarden/data/vault @bitwarden/team-vault-dev
# app/src/test/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
# Docker-related files
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre

View File

@@ -4,12 +4,12 @@ inputs:
java-version:
description: 'Java version to use'
required: false
default: '21'
default: '17'
runs:
using: 'composite'
steps:
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -31,12 +31,12 @@ runs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ inputs.java-version }}

View File

@@ -27,9 +27,6 @@
],
"matchManagers": [
"gradle"
],
"excludePackageNames": [
"com.github.bumptech.glide:compose"
]
},
{

View File

@@ -1,159 +0,0 @@
name: Calculate Version Name and Number
on:
workflow_dispatch:
inputs:
app_codename:
description: "App Name - e.g. 'bwpm' or 'bwa'"
base_version_number:
description: "Base Version Number - Will be added to the calculated version number"
type: number
default: 0
version_name:
description: "Version Name Override - e.g. '2024.8.1'"
version_number:
description: "Version Number Override - e.g. '1021'"
patch_version:
description: "Patch Version Override - e.g. '999'"
distinct_id:
description: "Unique ID for this dispatch, used by dispatch-and-download.yml"
skip_checkout:
description: "Skip checking out the repository"
type: boolean
workflow_call:
inputs:
app_codename:
description: "App Name - e.g. 'bwpm' or 'bwa'"
type: string
base_version_number:
description: "Base Version Number - Will be added to the calculated version number"
type: number
default: 0
version_name:
description: "Version Name Override - e.g. '2024.8.1'"
type: string
version_number:
description: "Version Number Override - e.g. '1021'"
type: string
patch_version:
description: "Patch Version Override - e.g. '999'"
type: string
distinct_id:
description: "Unique ID for this dispatch, used by dispatch-and-download.yml"
type: string
skip_checkout:
description: "Skip checking out the repository"
type: boolean
outputs:
version_name:
description: "Version Name"
value: ${{ jobs.calculate-version.outputs.version_name }}
version_number:
description: "Version Number"
value: ${{ jobs.calculate-version.outputs.version_number }}
env:
APP_CODENAME: ${{ inputs.app_codename }}
BASE_VERSION_NUMBER: ${{ inputs.base_version_number || 0 }}
jobs:
calculate-version:
name: Calculate Version Name and Number
runs-on: ubuntu-22.04
permissions:
contents: read
outputs:
version_name: ${{ steps.calc-version-name.outputs.version_name }}
version_number: ${{ steps.calc-version-number.outputs.version_number }}
steps:
- name: Log inputs to job summary
uses: bitwarden/android/.github/actions/log-inputs@main
with:
inputs: "${{ toJson(inputs) }}"
- name: Echo distinct ID ${{ github.event.inputs.distinct_id }}
run: echo ${{ github.event.inputs.distinct_id }}
- name: Check out repository
if: ${{ !inputs.skip_checkout || false }}
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Calculate version name
id: calc-version-name
run: |
output() {
local version_name=$1
echo "version_name=$version_name" >> $GITHUB_OUTPUT
}
# override version name if provided
if [[ ! -z "${{ inputs.version_name }}" ]]; then
version_name=${{ inputs.version_name }}
echo "::warning::Override applied: $version_name"
output "$version_name"
exit 0
fi
current_year=$(date +%Y)
current_month=$(date +%-m)
latest_tag_version=$(git tag -l --sort=-creatordate | grep "$APP_CODENAME" | head -n 1)
if [[ -z "$latest_tag_version" ]]; then
version_name="${current_year}.${current_month}.${{ inputs.patch_version || 0 }}"
echo "::warning::No tags found, did you checkout? Calculating version from current date: $version_name"
output "$version_name"
exit 0
fi
# Git tag was found, calculate version from latest tag
latest_version=${latest_tag_version:1} # remove 'v' from tag version
latest_major_version=$(echo $latest_version | cut -d "." -f 1)
latest_minor_version=$(echo $latest_version | cut -d "." -f 2)
patch_version=0
if [[ ! -z "${{ inputs.patch_version }}" ]]; then
patch_version=${{ inputs.patch_version }}
echo "::warning::Patch Version Override applied: $patch_version"
elif [[ "$current_year" == "$latest_major_version" && "$current_month" == "$latest_minor_version" ]]; then
latest_patch_version=$(echo $latest_version | cut -d "." -f 3)
patch_version=$(($latest_patch_version + 1))
fi
version_name="${current_year}.${current_month}.${patch_version}"
output "$version_name"
- name: Calculate version number
id: calc-version-number
run: |
# override version number if provided
if [[ ! -z "${{ inputs.version_number }}" ]]; then
version_number=${{ inputs.version_number }}
echo "::warning::Override applied: $version_number"
echo "version_number=$version_number" >> $GITHUB_OUTPUT
exit 0
fi
version_number=$(($GITHUB_RUN_NUMBER + ${{ env.BASE_VERSION_NUMBER }}))
echo "version_number=$version_number" >> $GITHUB_OUTPUT
- name: Create version info JSON
run: |
json='{
"version_number": "${{ steps.calc-version-number.outputs.version_number }}",
"version_name": "${{ steps.calc-version-name.outputs.version_name }}"
}'
echo "$json" > version_info.json
echo "## version-info.json" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo "$json" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Upload version info artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: version-info
path: version_info.json

View File

@@ -15,9 +15,6 @@ on:
description: "Optional. Build number to use. Overrides default of GitHub run number."
required: false
type: number
patch_version:
description: "Order 999 - Overrides Patch version"
type: boolean
distribute-to-firebase:
description: "Optional. Distribute artifacts to Firebase."
required: false
@@ -31,7 +28,7 @@ on:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 21
JAVA_VERSION: 17
permissions:
contents: read
@@ -39,42 +36,25 @@ permissions:
id-token: write
jobs:
version:
name: Calculate Version Name and Number
uses: bitwarden/android/.github/workflows/_version.yml@main
with:
app_codename: "bwa"
base_version_number: 0
version_name: ${{ inputs.version-name }}
version_number: ${{ inputs.version-code }}
patch_version: ${{ inputs.patch_version && '999' || '' }}
secrets: inherit
build:
name: Build Authenticator
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
env:
INPUTS: ${{ toJson(inputs) }}
run: |
{
echo "<details><summary>Job Inputs</summary>"
echo ""
echo '```json'
echo "$INPUTS"
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -96,13 +76,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
@@ -121,7 +101,6 @@ jobs:
publish_playstore:
name: Publish Authenticator Play Store artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
strategy:
@@ -131,12 +110,10 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
@@ -168,27 +145,27 @@ jobs:
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/keystores
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_apk-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_apk-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_aab-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_aab-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name com.bitwarden.authenticator-google-services.json --file ${{ github.workspace }}/authenticator/src/google-services.json --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name com.bitwarden.authenticator.dev-google-services.json --file ${{ github.workspace }}/authenticator/src/debug/google-services.json --output none
- name: Download Firebase credentials
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if : ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_firebase-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json --output none
- name: Download Play Store credentials
@@ -199,7 +176,7 @@ jobs:
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: AZ Logout
@@ -209,10 +186,10 @@ jobs:
if: ${{ inputs.publish-to-play-store }}
run: |
bundle exec fastlane run validate_play_store_json_key \
json_key:"${{ github.workspace }}/secrets/authenticator_play_store-creds.json"
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -234,7 +211,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -242,54 +219,44 @@ jobs:
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
- name: Increment version
env:
DEFAULT_VERSION_CODE: ${{ github.run_number }}
INPUT_VERSION_CODE: "${{ needs.version.outputs.version_number }}"
INPUT_VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: |
VERSION_CODE="${INPUT_VERSION_CODE:-$DEFAULT_VERSION_CODE}"
VERSION_NAME_INPUT="${INPUT_VERSION_NAME:-}"
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME_INPUT"
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> "$GITHUB_STEP_SUMMARY"
echo "Version Number: $VERSION_CODE" >> "$GITHUB_STEP_SUMMARY"
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate release Play Store bundle
if: ${{ matrix.variant == 'aab' }}
env:
STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}
KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}
run: |
bundle exec fastlane bundleAuthenticatorRelease \
storeFile:"${{ github.workspace }}/keystores/authenticator_aab-keystore.jks" \
storePassword:"$STORE_PASSWORD" \
keyAlias:"authenticatorupload" \
keyPassword:"$KEY_PASSWORD"
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:authenticatorupload \
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
- name: Generate release Play Store APK
if: ${{ matrix.variant == 'apk' }}
env:
STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}
KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}
run: |
bundle exec fastlane buildAuthenticatorRelease \
storeFile:"${{ github.workspace }}/keystores/authenticator_apk-keystore.jks" \
storePassword:"$STORE_PASSWORD" \
keyAlias:"bitwardenauthenticator" \
keyPassword:"$KEY_PASSWORD"
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:bitwardenauthenticator \
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
- name: Upload release Play Store .aab artifact
if: ${{ matrix.variant == 'aab' }}
@@ -345,7 +312,7 @@ jobs:
FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json
run: |
bundle exec fastlane distributeAuthenticatorReleaseBundleToFirebase \
serviceCredentialsFile:"$FIREBASE_CREDS_PATH"
serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }}
# Only publish bundles to Play Store when `publish-to-play-store` is true while building
# bundles
@@ -355,4 +322,4 @@ jobs:
PLAY_STORE_CREDS_FILE: ${{ github.workspace }}/secrets/authenticator_play_store-creds.json
run: |
bundle exec fastlane publishAuthenticatorReleaseToGooglePlayStore \
serviceCredentialsFile:"$PLAY_STORE_CREDS_FILE" \
serviceCredentialsFile:${{ env.PLAY_STORE_CREDS_FILE }} \

View File

@@ -15,23 +15,20 @@ on:
description: "Optional. Build number to use. Overrides default of GitHub run number."
required: false
type: number
patch_version:
description: "Order 999 - Overrides Patch version"
type: boolean
distribute-to-firebase:
description: "Optional. Distribute artifacts to Firebase."
required: false
default: true
default: false
type: boolean
publish-to-play-store:
description: "Optional. Deploy bundle artifact to Google Play Store"
required: false
default: true
default: false
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 21
JAVA_VERSION: 17
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions:
@@ -40,43 +37,25 @@ permissions:
id-token: write
jobs:
version:
name: Calculate Version Name and Number
uses: bitwarden/android/.github/workflows/_version.yml@main
with:
app_codename: "bwpm"
# Start from 11000 to prevent collisions with mobile build version codes
base_version_number: 11000
version_name: ${{ inputs.version-name }}
version_number: ${{ inputs.version-code }}
patch_version: ${{ inputs.patch_version && '999' || '' }}
secrets: inherit
build:
name: Build
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
env:
INPUTS: ${{ toJson(inputs) }}
run: |
{
echo "<details><summary>Job Inputs</summary>"
echo ""
echo '```json'
echo "$INPUTS"
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -98,13 +77,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
@@ -130,7 +109,6 @@ jobs:
publish_playstore:
name: Publish Play Store artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
strategy:
@@ -140,12 +118,10 @@ jobs:
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
@@ -178,19 +154,19 @@ jobs:
mkdir -p ${{ github.workspace }}/app/src/standardBeta
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_upload-keystore.jks --file ${{ github.workspace }}/keystores/app_upload-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_beta_play-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_play-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_beta_upload-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_upload-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name google-services.json --file ${{ github.workspace }}/app/src/standardBeta/google-services.json --output none
- name: Download Firebase credentials
@@ -201,14 +177,14 @@ jobs:
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -230,7 +206,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -238,67 +214,64 @@ jobs:
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
- name: Increment version
env:
VERSION_CODE: ${{ needs.version.outputs.version_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: |
VERSION_CODE="${VERSION_CODE:-$GITHUB_RUN_NUMBER}"
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionName:$VERSION_NAME
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionName:${{ inputs.version-name }}
- name: Generate release Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
env:
UPLOAD_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreRelease \
storeFile:app_upload-keystore.jks \
storePassword:$UPLOAD_KEYSTORE_PASSWORD \
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
keyAlias:upload \
keyPassword:$UPLOAD_KEYSTORE_PASSWORD
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
- name: Generate beta Play Store bundle
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
env:
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
UPLOAD_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreBeta \
storeFile:app_beta_upload-keystore.jks \
storePassword:$UPLOAD_BETA_KEYSTORE_PASSWORD \
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden-beta-upload \
keyPassword:$UPLOAD_BETA_KEY_PASSWORD
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
- name: Generate release Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreReleaseApk \
storeFile:app_play-keystore.jks \
storePassword:$PLAY_KEYSTORE_PASSWORD \
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden \
keyPassword:$PLAY_KEYSTORE_PASSWORD
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
- name: Generate beta Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreBetaApk \
storeFile:app_beta_play-keystore.jks \
storePassword:$PLAY_BETA_KEYSTORE_PASSWORD \
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden-beta \
keyPassword:$PLAY_BETA_KEY_PASSWORD
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
- name: Generate debug Play Store APKs
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
@@ -426,8 +399,8 @@ jobs:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
bundle exec fastlane distributeReleasePlayStoreToFirebase \
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_PLAY_FIREBASE_CREDS_PATH
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
- name: Publish beta artifacts to Firebase
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
@@ -435,8 +408,8 @@ jobs:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
bundle exec fastlane distributeBetaPlayStoreToFirebase \
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_PLAY_FIREBASE_CREDS_PATH
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
- name: Verify Play Store credentials
if: ${{ matrix.variant == 'prod' && inputs.publish-to-play-store }}
@@ -444,7 +417,7 @@ jobs:
bundle exec fastlane run validate_play_store_json_key
- name: Publish Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
run: |
bundle exec fastlane publishProdToPlayStore
bundle exec fastlane publishBetaToPlayStore
@@ -452,17 +425,14 @@ jobs:
publish_fdroid:
name: Publish F-Droid artifacts
needs:
- version
- build
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
@@ -491,9 +461,9 @@ jobs:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_fdroid-keystore.jks --file ${{ github.workspace }}/keystores/app_fdroid-keystore.jks --output none
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_beta_fdroid-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_fdroid-keystore.jks --output none
- name: Download Firebase credentials
@@ -504,14 +474,14 @@ jobs:
run: |
mkdir -p ${{ github.workspace }}/secrets
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -533,7 +503,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -541,48 +511,47 @@ jobs:
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
"$GITHUB_REPOSITORY" \
"$GITHUB_REF_NAME" \
"$GITHUB_SHA" \
"$GITHUB_RUN_ID" \
"$GITHUB_RUN_ATTEMPT"
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
# Start from 11000 to prevent collisions with mobile build version codes
- name: Increment version
env:
VERSION_CODE: ${{ needs.version.outputs.version_number }}
VERSION_NAME: ${{ needs.version.outputs.version_name }}
run: |
VERSION_CODE="${VERSION_CODE:-$GITHUB_RUN_NUMBER}"
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionName:$VERSION_NAME
versionName:${{ inputs.version-name || '' }}
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> "$GITHUB_STEP_SUMMARY"
echo "Version Number: $VERSION_CODE" >> "$GITHUB_STEP_SUMMARY"
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidReleaseApk \
storeFile:app_fdroid-keystore.jks \
storePassword:$FDROID_STORE_PASSWORD \
storePassword:"${{ env.FDROID_STORE_PASSWORD }}" \
keyAlias:bitwarden \
keyPassword:$FDROID_STORE_PASSWORD
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
- name: Generate F-Droid Beta Artifacts
env:
FDROID_BETA_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
FDROID_BETA_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidBetaApk \
storeFile:app_beta_fdroid-keystore.jks \
storePassword:$FDROID_BETA_KEYSTORE_PASSWORD \
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
keyAlias:bitwarden-beta \
keyPassword:$FDROID_BETA_KEY_PASSWORD
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -632,5 +601,5 @@ jobs:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |
bundle exec fastlane distributeReleaseFDroidToFirebase \
actionUrl:$GITHUB_ACTION_RUN_URL \
service_credentials_file:$APP_FDROID_FIREBASE_CREDS_PATH
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }}

View File

@@ -3,7 +3,7 @@ name: Cron / Sync Google Privileged Browsers List
on:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: "0 0 * * 1"
- cron: '0 0 * * 1'
workflow_dispatch:
env:
@@ -21,26 +21,25 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: true
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Download Google Privileged Browsers List
run: curl -s "$SOURCE_URL" -o "$GOOGLE_FILE"
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
- name: Check for changes
id: check-changes
run: |
if git diff --quiet -- "$GOOGLE_FILE"; then
if git diff --quiet -- $GOOGLE_FILE; then
echo "👀 No changes detected, skipping..."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "👀 Changes detected, validating fido2_privileged_google.json..."
if ! python .github/scripts/validate-json/validate_json.py validate "$GOOGLE_FILE"; then
python .github/scripts/validate-json/validate_json.py validate $GOOGLE_FILE
if [ $? -ne 0 ]; then
echo "::error::JSON validation failed for $GOOGLE_FILE"
exit 1
fi
@@ -48,14 +47,14 @@ jobs:
echo "👀 fido2_privileged_google.json is valid, checking for duplicates..."
# Check for duplicates between Google and Community files
python .github/scripts/validate-json/validate_json.py duplicates "$GOOGLE_FILE" "$COMMUNITY_FILE" duplicates.txt
python .github/scripts/validate-json/validate_json.py duplicates $GOOGLE_FILE $COMMUNITY_FILE duplicates.txt
if [ -f duplicates.txt ]; then
echo "::warning::Duplicate package names found between Google and Community files."
echo "duplicates_found=true" >> "$GITHUB_OUTPUT"
echo "duplicates_found=true" >> $GITHUB_OUTPUT
else
echo "✅ No duplicate package names found between Google and Community files"
echo "duplicates_found=false" >> "$GITHUB_OUTPUT"
echo "duplicates_found=false" >> $GITHUB_OUTPUT
fi
- name: Create branch and commit
@@ -66,11 +65,11 @@ jobs:
BRANCH_NAME="cron-sync-privileged-browsers/$GITHUB_RUN_NUMBER-sync"
git config user.name "GitHub Actions Bot"
git config user.email "actions@github.com"
git checkout -b "$BRANCH_NAME"
git add "$GOOGLE_FILE"
git checkout -b $BRANCH_NAME
git add $GOOGLE_FILE
git commit -m "Update Google privileged browsers list"
git push origin "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
git push origin $BRANCH_NAME
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "🌱 Branch created: $BRANCH_NAME"
- name: Create Pull Request
@@ -90,10 +89,10 @@ jobs:
fi
# Use echo -e to interpret escape sequences and pipe to gh pr create
echo -e "$PR_BODY" | gh pr create \
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update Google privileged browsers list" \
--body-file - \
--base main \
--head "$BRANCH_NAME" \
--head $BRANCH_NAME \
--label "automated-pr" \
--label "t:ci"
--label "t:ci")

View File

@@ -4,7 +4,7 @@ run-name: Crowdin Pull - ${{ github.event_name == 'workflow_dispatch' && 'Manual
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 5"
- cron: '0 0 * * 5'
jobs:
crowdin-sync:
@@ -16,9 +16,7 @@ jobs:
id-token: write
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -52,7 +50,7 @@ jobs:
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Download translations
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -16,9 +16,7 @@ jobs:
id-token: write
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -35,7 +33,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -4,16 +4,16 @@ on:
workflow_dispatch:
inputs:
artifact-run-id:
description: "GitHub Action Run ID containing artifacts"
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
release-ticket-id:
description: "Release Ticket ID - e.g. RELEASE-1762"
description: 'Release Ticket ID - e.g. RELEASE-1762'
required: true
type: string
env:
ARTIFACTS_PATH: artifacts
ARTIFACTS_PATH: artifacts
jobs:
create-release:
@@ -25,10 +25,9 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: true
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
@@ -41,7 +40,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
workflow_data=$(gh run view "$ARTIFACT_RUN_ID" --json headBranch,workflowName)
workflow_data=$(gh run view $ARTIFACT_RUN_ID --json headBranch,workflowName)
release_branch=$(echo "$workflow_data" | jq -r .headBranch)
workflow_name=$(echo "$workflow_data" | jq -r .workflowName)
@@ -53,8 +52,8 @@ jobs:
echo "🔖 Release branch: $release_branch"
echo "🔖 Workflow name: $workflow_name"
echo "release_branch=$release_branch" >> "$GITHUB_OUTPUT"
echo "workflow_name=$workflow_name" >> "$GITHUB_OUTPUT"
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
case "$workflow_name" in
*"Password Manager"* | "Build")
@@ -72,8 +71,8 @@ jobs:
esac
echo "🔖 App name: $app_name"
echo "🔖 App name suffix: $app_name_suffix"
echo "app_name=$app_name" >> "$GITHUB_OUTPUT"
echo "app_name_suffix=$app_name_suffix" >> "$GITHUB_OUTPUT"
echo "app_name=$app_name" >> $GITHUB_OUTPUT
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
- name: Get version info from run logs and set release tag name
id: get_release_info
@@ -82,7 +81,7 @@ jobs:
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_APP_NAME_SUFFIX: ${{ steps.get_release_branch.outputs.app_name_suffix }}
run: |
workflow_log=$(gh run view "$ARTIFACT_RUN_ID" --log)
workflow_log=$(gh run view $ARTIFACT_RUN_ID --log)
version_number_with_trailing_dot=$(grep -m 1 "Setting version code to" <<< "$workflow_log" | sed 's/.*Setting version code to //')
version_number=${version_number_with_trailing_dot%.} # remove trailing dot
@@ -104,28 +103,28 @@ jobs:
echo "✅ Found version number: $version_number"
fi
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
echo "version_name=$version_name" >> "$GITHUB_OUTPUT"
echo "version_number=$version_number" >> $GITHUB_OUTPUT
echo "version_name=$version_name" >> $GITHUB_OUTPUT
tag_name="v$version_name-$_APP_NAME_SUFFIX" # e.g. v2025.6.0-bwpm
echo "🔖 New tag name: $tag_name"
echo "tag_name=$tag_name" >> "$GITHUB_OUTPUT"
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
last_release_tag=$(git tag -l --sort=-authordate | grep "$_APP_NAME_SUFFIX" | head -n 1)
echo "🔖 Last release tag: $last_release_tag"
echo "last_release_tag=$last_release_tag" >> "$GITHUB_OUTPUT"
echo "last_release_tag=$last_release_tag" >> $GITHUB_OUTPUT
- name: Download artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
gh run download "$ARTIFACT_RUN_ID" -D "$ARTIFACTS_PATH"
file_count=$(find "$ARTIFACTS_PATH" -type f | wc -l)
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
echo "Downloaded $file_count file(s)."
if [ "$file_count" -gt 0 ]; then
echo "Downloaded files:"
find "$ARTIFACTS_PATH" -type f
find $ARTIFACTS_PATH -type f
fi
# Files that won't be included in any release
@@ -149,12 +148,12 @@ jobs:
)
for file in "${files_to_remove[@]}"; do
find "$ARTIFACTS_PATH" -name "$file" -type f -delete
find $ARTIFACTS_PATH -name "$file" -type f -delete
done
echo "🔖 Removed internal artifacts."
echo ""
echo "🔖 Files to be included in the release:"
find "$ARTIFACTS_PATH" -type f
find $ARTIFACTS_PATH -type f
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -184,7 +183,7 @@ jobs:
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
run: |
echo "Getting product release notes"
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py "$_RELEASE_TICKET_ID" "$_JIRA_API_EMAIL" "$_JIRA_API_TOKEN")
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
if [[ -z "$product_release_notes" || $product_release_notes == "Error checking"* ]]; then
echo "::warning::Failed to fetch release notes from Jira. Output: $product_release_notes"
@@ -220,12 +219,12 @@ jobs:
--notes-start-tag "$_LAST_RELEASE_TAG" \
--latest=$is_latest_release \
--draft \
"$ARTIFACTS_PATH/*/*")
$ARTIFACTS_PATH/*/*)
# Extract release tag from URL
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
echo "release_id_from_url=$release_id_from_url" >> "$GITHUB_OUTPUT"
echo "url=$release_url" >> "$GITHUB_OUTPUT"
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
echo "url=$release_url" >> $GITHUB_OUTPUT
echo "✅ Release created: $release_url"
echo "🔖 Release ID from URL: $release_id_from_url"
@@ -253,7 +252,7 @@ jobs:
new_release_url=$(gh release edit "$_RELEASE_ID" --notes "$updated_body")
# draft release links change after editing
echo "release_url=$new_release_url" >> "$GITHUB_OUTPUT"
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
- name: Add Release Summary
env:
@@ -264,26 +263,20 @@ jobs:
_RELEASE_BRANCH: ${{ steps.get_release_branch.outputs.release_branch }}
_RELEASE_URL: ${{ steps.update_release_description.outputs.release_url }}
run: |
{
echo "# :fish_cake: Release ready at:"
echo "$_RELEASE_URL"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
echo "# :fish_cake: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$_RELEASE_URL" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "$_VERSION_NAME" == "0.0.0" || "$_VERSION_NUMBER" == "0" ]]; then
{
echo "> [!CAUTION]"
echo "> Version name or number wasn't previously found and a default value was used. You'll need to manually update the release Title, Tag and Description, specifically, the \"Full Changelog\" link."
echo ""
} >> "$GITHUB_STEP_SUMMARY"
echo "> [!CAUTION]" >> $GITHUB_STEP_SUMMARY
echo "> Version name or number wasn't previously found and a default value was used. You'll need to manually update the release Title, Tag and Description, specifically, the "Full Changelog" link." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
{
echo ":clipboard: Confirm that the defined GitHub Release options are correct:"
echo " * :bookmark: New tag name: \`$_RELEASE_TAG\`"
echo " * :palm_tree: Target branch: \`$_RELEASE_BRANCH\`"
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`"
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch"
echo "> [!NOTE]"
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes."
} >> "$GITHUB_STEP_SUMMARY"
echo ":clipboard: Confirm that the defined GitHub Release options are correct:" >> $GITHUB_STEP_SUMMARY
echo " * :bookmark: New tag name: \`$_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :palm_tree: Target branch: \`$_RELEASE_BRANCH\`" >> $GITHUB_STEP_SUMMARY
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch" >> $GITHUB_STEP_SUMMARY
echo "> [!NOTE]" >> $GITHUB_STEP_SUMMARY
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,23 +0,0 @@
name: Publish Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
permissions:
contents: write
id-token: write
actions: write
jobs:
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release-bwa.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
secrets: inherit

View File

@@ -1,24 +0,0 @@
name: Publish Password Manager GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
permissions:
contents: write
id-token: write
actions: write
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release-bwpm.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit

View File

@@ -0,0 +1,36 @@
name: Publish Password Manager and Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 3 * * 1-5'
permissions:
contents: write
id-token: write
actions: read
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
secrets: inherit

View File

@@ -1,6 +1,5 @@
name: Publish to Google Play
run-name: >
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
on:
workflow_dispatch:
inputs:
@@ -18,15 +17,15 @@ on:
required: true
type: string
rollout-percentage:
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
release-notes:
description: "Change notes to be included with this release."
type: string
@@ -47,10 +46,6 @@ on:
- production
- Fastlane Automation Target
required: true
dry-run:
description: "Dry-Run, Run the workflow without publishing to the store"
type: boolean
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
@@ -59,132 +54,107 @@ permissions:
contents: read
packages: read
id-token: write
actions: write
jobs:
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: Log inputs to job summary
env:
INPUTS: ${{ toJson(inputs) }}
run: |
{
echo "<details><summary>Job Inputs</summary>"
echo ""
echo '```json'
echo "$INPUTS"
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Configure Ruby
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: Format Release Notes
env:
RELEASE_NOTES: ${{ inputs.release-notes }}
run: |
FORMATTED_MESSAGE="$(echo "$RELEASE_NOTES" | sed 's/ /\n/g')"
{
echo "RELEASE_NOTES<<EOF"
printf '%s\n' "$FORMATTED_MESSAGE"
echo "EOF"
} >> "$GITHUB_ENV"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
- name: Format Release Notes
run: |
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE" \
packageName:"$PACKAGE_NAME"
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE" \
packageName:"$PACKAGE_NAME"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"
- name: Enable Publish Github Release Workflow
env:
PRODUCT: ${{ inputs.product }}
run: |
if ${{ inputs.dry-run }} ; then
gh workflow view publish-github-release-bwpm.yml
exit 0
fi
if [ "$PRODUCT" = "Password Manager" ]; then
gh workflow enable publish-github-release-bwpm.yml
elif [ "$PRODUCT" = "Authenticator" ]; then
gh workflow enable publish-github-release-bwa.yml
fi
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
release_type:
description: "Release Type"
description: 'Release Type'
required: true
type: choice
options:
@@ -22,10 +22,9 @@ jobs:
actions: write
steps:
- name: Check out repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
persist-credentials: true
- name: Create RC or Test Branch
id: rc_branch
@@ -43,10 +42,10 @@ jobs:
branch_name="release/${branch_name}"
git switch main
git switch -c "$branch_name"
git push origin "$branch_name"
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
- name: Create Hotfix Branch
id: hotfix_branch
@@ -67,14 +66,14 @@ jobs:
fi
branch_name="release/hotfix-${latest_tag}"
echo "🌿 branch name: $branch_name"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
git switch -c "$branch_name" "$latest_tag"
git push origin "$branch_name"
echo "# :fire: Hotfix branch: ${branch_name}" >> "$GITHUB_STEP_SUMMARY"
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
- name: Trigger CI Workflows
env:
@@ -82,5 +81,5 @@ jobs:
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
run: |
echo "🌿 branch name: $_BRANCH_NAME"
gh workflow run build.yml --ref "$_BRANCH_NAME" -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build-authenticator.yml --ref "$_BRANCH_NAME" -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true

View File

@@ -1,28 +0,0 @@
name: Respond
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions: {}
jobs:
respond:
name: Respond
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write

View File

@@ -1,20 +0,0 @@
name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions: {}
jobs:
review:
name: Review
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
contents: read
id-token: write
pull-requests: write

View File

@@ -6,7 +6,7 @@ on:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target: # zizmor: ignore[dangerous-triggers]
pull_request_target:
types: [opened, synchronize, reopened]
branches:
- main

View File

@@ -22,10 +22,6 @@ on:
pr-id:
description: "Pull Request ID"
env:
_BOT_NAME: "bw-ghapp[bot]"
_BOT_EMAIL: "178206702+bw-ghapp[bot]@users.noreply.github.com"
jobs:
update:
name: Update and PR
@@ -58,16 +54,11 @@ jobs:
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-pull-requests: write
permission-actions: read
permission-contents: write
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
persist-credentials: true
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
@@ -78,61 +69,18 @@ jobs:
id: switch-branch
run: |
BRANCH_NAME="sdlc/sdk-update"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
git switch -c $BRANCH_NAME
if git switch "$BRANCH_NAME"; then
echo "✅ Switched to existing branch: $BRANCH_NAME"
echo "updating_existing_branch=true" >> "$GITHUB_OUTPUT"
else
echo "📝 Creating new branch: $BRANCH_NAME"
git switch -c "$BRANCH_NAME"
echo "updating_existing_branch=false" >> "$GITHUB_OUTPUT"
fi
- name: Prevent updating the branch when the last committer isn't the bot
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
run: |
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' "$_BRANCH_NAME")
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
echo "Expected bot email: $_BOT_EMAIL"
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
"This indicates manual changes have been made to the branch," \
"PR has to be merged or closed before running this workflow again."
echo "👀 Fetching existing PR..."
gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty'
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
if [ -z "$EXISTING_PR" ]; then
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
exit 1
fi
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ❌ Merge or close: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
# Using main to retrieve the changelog on consecutive updates of the same PR.
- name: Get current SDK version from main branch
- name: Get current SDK version
id: get-current-sdk
run: |
git show origin/main:gradle/libs.versions.toml
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
if [ -z "$SDK_VERSION" ]; then
echo "::error::Failed to get current SDK version from main branch."
exit 1
fi
SDK_VERSION=$(grep "bitwardenSdk =" gradle/libs.versions.toml | cut -d'"' -f2)
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
echo "Current SDK version (from main): $SDK_VERSION"
echo "Current SDK version: $SDK_VERSION"
echo "Current SDK git ref: $GIT_REF"
echo "version=$SDK_VERSION" >> "$GITHUB_OUTPUT"
echo "git_ref=$GIT_REF" >> "$GITHUB_OUTPUT"
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
- name: Update SDK Version
env:
@@ -149,14 +97,14 @@ jobs:
run: |
echo "👀 Committing SDK version update..."
git config user.name "$_BOT_NAME"
git config user.email "$_BOT_EMAIL"
git config user.name "bw-ghapp[bot]"
git config user.email "178206702+bw-ghapp[bot]@users.noreply.github.com"
git add gradle/libs.versions.toml
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
git push origin "$_BRANCH_NAME"
git push origin $_BRANCH_NAME
- name: Create or Update Pull Request
- name: Create Pull Request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
@@ -173,26 +121,17 @@ jobs:
$CHANGELOG"
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
# Use echo -e to interpret escape sequences and pipe to gh pr create
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update SDK to $_SDK_VERSION" \
--body-file - \
--base main \
--head $_BRANCH_NAME \
--label "automated-pr" \
--label "t:ci")
if [ -n "$EXISTING_PR" ]; then
echo "🔄 Updating existing PR #$EXISTING_PR..."
echo -e "$PR_BODY" | gh pr edit "$EXISTING_PR" \
--title "Update SDK to $_SDK_VERSION" \
--body-file -
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ✅ Updated PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
else
echo "📝 Creating new PR..."
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update SDK to $_SDK_VERSION" \
--body-file - \
--base main \
--head "$_BRANCH_NAME" \
--label "automated-pr" \
--label "t:ci")
echo "## 🚀 Created PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
fi
echo "🚀 Created PR: $PR_URL"
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
test:
name: Test Update
@@ -204,9 +143,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs

View File

@@ -13,9 +13,10 @@ on:
workflow_dispatch:
env:
_JAVA_VERSION: 21
_JAVA_VERSION: 17
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
jobs:
test:
name: Test
@@ -26,12 +27,10 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
@@ -53,12 +52,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: "temurin"
java-version: ${{ env._JAVA_VERSION }}
@@ -92,7 +91,7 @@ jobs:
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
if: github.event_name == 'push' || github.event_name == 'pull_request'
continue-on-error: true
with:
@@ -104,14 +103,14 @@ jobs:
- name: Comment PR if tests failed
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
env:
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ACTOR: ${{ github.triggering_actor }}
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ACTOR: ${{ github.triggering_actor }}
run: |
echo "> [!WARNING]" >> "$GITHUB_STEP_SUMMARY"
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> "$GITHUB_STEP_SUMMARY"
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> $GITHUB_STEP_SUMMARY
if [ -n "$PR_NUMBER" ]; then
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Upload to codecov.io" step of [Process Test Reports job]('$_GITHUB_ACTION_RUN_URL') for more details.'
gh pr comment --repo "$GITHUB_REPOSITORY" "$PR_NUMBER" --body "$message"
gh pr comment --repo $GITHUB_REPOSITORY $PR_NUMBER --body "$message"
fi

5
.github/zizmor.yml vendored
View File

@@ -1,5 +0,0 @@
rules:
unpinned-uses:
config:
policies:
bitwarden/gh-actions/*: ref-pin

View File

@@ -11,8 +11,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1177.0)
aws-sdk-core (3.235.0)
aws-partitions (1.1139.0)
aws-sdk-core (3.228.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -20,18 +20,18 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.115.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (1.109.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.201.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-s3 (1.195.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.3.1)
bigdecimal (3.2.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -169,7 +169,7 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.15.2)
json (2.13.2)
jwt (2.10.2)
base64
logger (1.7.0)
@@ -192,15 +192,15 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.4)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.21.0)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList

View File

@@ -11,8 +11,8 @@ Bitwarden Authenticator allows you easily store and generate two-factor authenti
## Compatibility
- **Minimum SDK**: 28 (Android 9)
- **Target SDK**: 36 (Android 16)
- **Minimum SDK**: 28
- **Target SDK**: 34
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape

View File

@@ -4,12 +4,13 @@
- [Compatibility](#compatibility)
- [Setup](#setup)
- [Theme](#theme)
- [Dependencies](#dependencies)
## Compatibility
- **Minimum SDK**: 29 (Android 10)
- **Target SDK**: 36 (Android 16)
- **Minimum SDK**: 29
- **Target SDK**: 35
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape
@@ -51,12 +52,12 @@
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
4. Setup JDK `Version` `21`:
4. Setup JDK `Version` `17`:
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
- Hit the selected Gradle JDK next to `Gradle JDK:`.
- Select a `21.x` version or hit `Download JDK...` if not present.
- Select `Version` `21`.
- Select a `17.x` version or hit `Download JDK...` if not present.
- Select `Version` `17`.
- Select your preferred `Vendor`.
- Hit `Download`.
- Hit `Apply`.
@@ -92,6 +93,25 @@ chmod +x .git/hooks/pre-commit
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
```
## Theme
### Icons & Illustrations
The app supports light mode, dark mode and dynamic colors. Most icons in the app will display correctly using tinting but multi-tonal icons and illustrations require extra processing in order to be displayed properly with dynamic colors.
All illustrations and multi-tonal icons require the svg paths to be tagged with the `name` attribute in order for each individual path to be tinted the appropriate color. Any untagged path will not be tinted and the resulting image will be incorrect.
The supported tags are as follows:
* outline
* primary
* secondary
* tertiary
* accent
* logo
* navigation
* navigationActiveAccent
## Dependencies
### Application Dependencies

View File

@@ -224,7 +224,6 @@ dependencies {
implementation(project(":annotation"))
implementation(project(":core"))
implementation(project(":cxf"))
implementation(project(":data"))
implementation(project(":network"))
implementation(project(":ui"))
@@ -235,6 +234,8 @@ dependencies {
implementation(libs.androidx.browser)
implementation(libs.androidx.biometrics)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.material3)
@@ -244,8 +245,6 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.providerevents)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.compose)
@@ -259,6 +258,7 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.bitwarden.sdk)
implementation(libs.bumptech.glide)
implementation(libs.androidx.credentials)
implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler)
implementation(libs.kotlinx.collections.immutable)
@@ -269,6 +269,7 @@ dependencies {
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only
debugImplementation(libs.androidx.compose.ui.test.manifest)
@@ -289,7 +290,7 @@ dependencies {
testImplementation(libs.google.hilt.android.testing)
testImplementation(platform(libs.junit.bom))
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.junit5)
testImplementation(libs.junit.vintage)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk.mockk)
@@ -302,10 +303,7 @@ tasks {
useJUnitPlatform()
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
// Explicitly setting the user Country and Language because tests assume en-US
"-Duser.country=US" +
"-Duser.language=en"
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
}
}

View File

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

View File

@@ -105,17 +105,6 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="bitwarden" />
</intent-filter>
<!-- Handle Credential Exchange transfer requests -->
<intent-filter
android:autoVerify="true"
tools:ignore="AppLinkUrlError">
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:scheme="content"
tools:ignore="AppLinkUriRelativeFilterGroupError" />
</intent-filter>
</activity>
<activity
@@ -260,7 +249,7 @@
android:name="com.x8bit.bitwarden.AutofillTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/autofill_title"
android:label="@string/autofill"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>

View File

@@ -48,18 +48,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox.nightly",
"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": {

View File

@@ -11,7 +11,6 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.auth.AuthTabIntent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -39,7 +38,6 @@ import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScre
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
import com.x8bit.bitwarden.ui.platform.util.appLanguage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.map
@@ -70,16 +68,6 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
private val duoLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.DuoResult(it))
}
private val ssoLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.SsoResult(it))
}
private val webAuthnLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
mainViewModel.trySendAction(MainAction.WebAuthnResult(it))
}
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
var shouldShowSplashScreen = true
@@ -100,14 +88,7 @@ class MainActivity : AppCompatActivity() {
SetupEventsEffect(navController = navController)
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider(
featureFlagsState = state.featureFlagsState,
authTabLaunchers = AuthTabLaunchers(
duo = duoLauncher,
sso = ssoLauncher,
webAuthn = webAuthnLauncher,
),
) {
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{ mainViewModel.trySendAction(MainAction.ResumeScreenDataReceived(it)) }

View File

@@ -2,23 +2,17 @@ package com.x8bit.bitwarden
import android.content.Intent
import android.os.Parcelable
import androidx.browser.auth.AuthTabIntent
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.share.ShareManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResult
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
@@ -81,7 +75,7 @@ class MainViewModel @Inject constructor(
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val shareManager: ShareManager,
private val intentManager: IntentManager,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
@@ -185,9 +179,6 @@ class MainViewModel @Inject constructor(
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
is MainAction.DuoResult -> handleDuoResult(action)
is MainAction.SsoResult -> handleSsoResult(action)
is MainAction.WebAuthnResult -> handleWebAuthnResult(action)
is MainAction.Internal -> handleInternalAction(action)
}
}
@@ -216,20 +207,6 @@ class MainViewModel @Inject constructor(
settingsRepository.appLanguage = action.appLanguage
}
private fun handleDuoResult(action: MainAction.DuoResult) {
authRepository.setDuoCallbackTokenResult(
tokenResult = action.authResult.getDuoCallbackTokenResult(),
)
}
private fun handleSsoResult(action: MainAction.SsoResult) {
authRepository.setSsoCallbackResult(result = action.authResult.getSsoCallbackResult())
}
private fun handleWebAuthnResult(action: MainAction.WebAuthnResult) {
authRepository.setWebAuthResult(webAuthResult = action.authResult.getWebAuthResult())
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
@@ -295,7 +272,7 @@ class MainViewModel @Inject constructor(
val passwordlessRequestData = intent.getPasswordlessRequestDataIntentOrNull()
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = shareManager.getShareDataOrNull(intent = intent)
val shareData = intentManager.getShareDataFromIntent(intent)
val totpData: TotpData? =
// First grab TOTP URI directly from the intent data:
intent.getTotpDataOrNull()
@@ -318,7 +295,6 @@ class MainViewModel @Inject constructor(
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
@@ -442,16 +418,6 @@ class MainViewModel @Inject constructor(
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
importCredentialsRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.CredentialExchangeExport(
data = ImportCredentialsRequestData(
uri = importCredentialsRequest.uri,
requestJson = importCredentialsRequest.request.requestJson,
),
)
}
}
}
@@ -519,21 +485,6 @@ data class MainState(
* Models actions for the [MainActivity].
*/
sealed class MainAction {
/**
* Receive the result from the Duo login flow.
*/
data class DuoResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive the result from the SSO login flow.
*/
data class SsoResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive the result from the WebAuthn login flow.
*/
data class WebAuthnResult(val authResult: AuthTabIntent.AuthResult) : MainAction()
/**
* Receive first Intent by the application.
*/

View File

@@ -216,59 +216,25 @@ interface AuthDiskSource : AppIdProvider {
/**
* Retrieves a pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
fun getPinProtectedUserKey(userId: String): String?
/**
* Retrieves a pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelope(userId: String): String?
/**
* Stores a pin-protected user key for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKey] during the current app session.
*/
@Deprecated(
message = "Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
inMemoryOnly: Boolean = false,
)
/**
* Stores a pin-protected user key envelope for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKeyEnvelope] during the current app session.
*/
fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean = false,
)
/**
* Retrieves a flow for the pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a flow for the pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?>
/**
* Gets a two-factor auth token using a user's [email].
*/

View File

@@ -37,7 +37,6 @@ private const val INVALID_UNLOCK_ATTEMPTS_KEY = "invalidUnlockAttempts"
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey"
private const val PIN_PROTECTED_USER_KEY_KEY = "pinKeyEncryptedUserKey"
private const val PIN_PROTECTED_USER_KEY_KEY_ENVELOPE = "pinKeyEncryptedUserKeyEnvelope"
private const val ENCRYPTED_PIN_KEY = "protectedPin"
private const val ORGANIZATIONS_KEY = "organizations"
private const val ORGANIZATION_KEYS_KEY = "encOrgKeys"
@@ -68,7 +67,6 @@ class AuthDiskSourceImpl(
AuthDiskSource {
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
private val inMemoryPinProtectedUserKeyEnvelopes = mutableMapOf<String, String?>()
private val mutableShouldUseKeyConnectorFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableOrganizationsFlowMap =
@@ -84,8 +82,6 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyEnvelopeFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -146,7 +142,6 @@ class AuthDiskSourceImpl(
storeUserKey(userId = userId, userKey = null)
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePrivateKey(userId = userId, privateKey = null)
storeAccountKeys(userId = userId, accountKeys = null)
@@ -334,24 +329,10 @@ class AuthDiskSourceImpl(
getMutableBiometricUnlockKeyFlow(userId)
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
@Deprecated(
"Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
override fun getPinProtectedUserKeyEnvelope(userId: String): String? =
inMemoryPinProtectedUserKeyEnvelopes[userId]
?: getString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
)
@Deprecated(
"Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
override fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
@@ -366,32 +347,10 @@ class AuthDiskSourceImpl(
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
override fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean,
) {
inMemoryPinProtectedUserKeyEnvelopes[userId] = pinProtectedUserKeyEnvelope
if (inMemoryOnly) return
putString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
value = pinProtectedUserKeyEnvelope,
)
getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
}
@Deprecated(
"Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
override fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyEnvelopeFlow(userId)
.onSubscription { emit(getPinProtectedUserKeyEnvelope(userId = userId)) }
override fun getTwoFactorToken(email: String): String? =
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
@@ -620,12 +579,6 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePinProtectedUserKeyEnvelopeFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyEnvelopeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -27,12 +27,6 @@ enum class OnboardingStatus {
@SerialName("autofillSetup")
AUTOFILL_SETUP,
/**
* The user is completing the browser autofill service setup.
*/
@SerialName("browserAutofillSetup")
BROWSER_AUTOFILL_SETUP,
/**
* The user is completing the final step of the onboarding process.
*/

View File

@@ -1,12 +1,9 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk.util
import com.bitwarden.crypto.Kdf
import com.bitwarden.network.model.KdfJson
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.KdfTypeJson.ARGON2_ID
import com.bitwarden.network.model.KdfTypeJson.PBKDF2_SHA256
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_ARGON2_MEMORY
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_ARGON2_PARALLELISM
/**
* Convert a [Kdf] to a [KdfTypeJson].
@@ -16,36 +13,3 @@ fun Kdf.toKdfTypeJson(): KdfTypeJson =
is Kdf.Argon2id -> ARGON2_ID
is Kdf.Pbkdf2 -> PBKDF2_SHA256
}
/**
* Convert a [Kdf] to [KdfJson]
*/
fun Kdf.toKdfRequestModel(): KdfJson =
when (this) {
is Kdf.Argon2id -> KdfJson(
kdfType = toKdfTypeJson(),
iterations = iterations.toInt(),
memory = memory.toInt(),
parallelism = parallelism.toInt(),
)
is Kdf.Pbkdf2 -> KdfJson(
kdfType = toKdfTypeJson(),
iterations = iterations.toInt(),
memory = null,
parallelism = null,
)
}
/**
* Convert a [KdfJson] to a [Kdf].
*/
fun KdfJson.toKdf(): Kdf =
when (this.kdfType) {
ARGON2_ID -> Kdf.Argon2id(
iterations = iterations.toUInt(),
memory = memory?.toUInt() ?: DEFAULT_ARGON2_MEMORY.toUInt(),
parallelism = parallelism?.toUInt() ?: DEFAULT_ARGON2_PARALLELISM.toUInt(),
)
PBKDF2_SHA256 -> Kdf.Pbkdf2(iterations = iterations.toUInt())
}

View File

@@ -10,20 +10,19 @@ class AuthTokenManagerImpl(
private val authDiskSource: AuthDiskSource,
) : AuthTokenManager {
override fun getAuthTokenDataOrNull(userId: String): AuthTokenData? =
authDiskSource
.getAccountTokens(userId = userId)
?.takeIf { it.accessToken != null }
?.let {
AuthTokenData(
userId = userId,
accessToken = requireNotNull(it.accessToken),
expiresAtSec = it.expiresAtSec,
)
}
override fun getAuthTokenDataOrNull(): AuthTokenData? = authDiskSource
.userState
?.activeUserId
?.let(::getAuthTokenDataOrNull)
?.let { userId ->
authDiskSource
.getAccountTokens(userId = userId)
?.takeIf { it.accessToken != null }
?.let {
AuthTokenData(
userId = userId,
accessToken = requireNotNull(it.accessToken),
expiresAtSec = it.expiresAtSec,
)
}
}
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
/**
* An interface to manage the user KDF settings.
*/
interface KdfManager {
/**
* Checks if user's current KDF settings are below the minimums and needs update
*/
fun needsKdfUpdateToMinimums(): Boolean
/**
* Updates the user's KDF settings if below the minimums
*/
suspend fun updateKdfToMinimumsIfNeeded(password: String): UpdateKdfMinimumsResult
}

View File

@@ -1,103 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.UpdateKdfResponse
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.crypto.Kdf
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.MasterPasswordAuthenticationDataJson
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
import com.bitwarden.network.model.UpdateKdfJsonRequest
import com.bitwarden.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonKdfUpdatedMinimums
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import timber.log.Timber
import kotlin.collections.get
/**
* Default implementation of [KdfManager].
*/
class KdfManagerImpl(
private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val accountsService: AccountsService,
private val featureFlagManager: FeatureFlagManager,
) : KdfManager {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
override fun needsKdfUpdateToMinimums(): Boolean {
if (!featureFlagManager.getFeatureFlag(FlagKey.ForceUpdateKdfSettings)) {
return false
}
val account = authDiskSource
.userState
?.accounts
?.get(activeUserId)
?: return false
if (account.profile.userDecryptionOptions != null &&
!account.profile.userDecryptionOptions.hasMasterPassword
) {
return false
}
return account.profile.kdfType == KdfTypeJson.PBKDF2_SHA256 &&
account.profile.kdfIterations != null &&
account.profile.kdfIterations < DEFAULT_PBKDF2_ITERATIONS
}
override suspend fun updateKdfToMinimumsIfNeeded(password: String): UpdateKdfMinimumsResult {
val userId = activeUserId ?: return UpdateKdfMinimumsResult.ActiveAccountNotFound
if (!needsKdfUpdateToMinimums()) {
return UpdateKdfMinimumsResult.Success
}
return vaultSdkSource
.makeUpdateKdf(
userId = userId,
password = password,
kdf = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
)
.flatMap {
accountsService.updateKdf(createUpdateKdfRequest(it))
}
.fold(
onSuccess = {
authDiskSource.userState = authDiskSource.userState
?.toUserStateJsonKdfUpdatedMinimums()
Timber.d("[Auth] Upgraded user's KDF to minimums")
UpdateKdfMinimumsResult.Success
},
onFailure = { UpdateKdfMinimumsResult.Error(error = it) },
)
}
private fun createUpdateKdfRequest(response: UpdateKdfResponse): UpdateKdfJsonRequest {
val authData = response.masterPasswordAuthenticationData
val oldAuthData = response.oldMasterPasswordAuthenticationData
val unlockData = response.masterPasswordUnlockData
return UpdateKdfJsonRequest(
authenticationData = MasterPasswordAuthenticationDataJson(
kdf = authData.kdf.toKdfRequestModel(),
masterPasswordAuthenticationHash = authData.masterPasswordAuthenticationHash,
salt = authData.salt,
),
key = unlockData.masterKeyWrappedUserKey,
masterPasswordHash = oldAuthData.masterPasswordAuthenticationHash,
newMasterPasswordHash = authData.masterPasswordAuthenticationHash,
unlockData = MasterPasswordUnlockDataJson(
kdf = unlockData.kdf.toKdfRequestModel(),
masterKeyWrappedUserKey = unlockData.masterKeyWrappedUserKey,
salt = unlockData.salt,
),
)
}
}

View File

@@ -10,7 +10,6 @@ import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManager
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -35,12 +34,10 @@ class UserLogoutManagerImpl(
private val toastManager: ToastManager,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val credentialExchangeRegistryManager: CredentialExchangeRegistryManager,
dispatcherManager: DispatcherManager,
) : UserLogoutManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val scope = CoroutineScope(dispatcherManager.unconfined)
private val mainScope = CoroutineScope(dispatcherManager.main)
private val ioScope = CoroutineScope(dispatcherManager.io)
private val mutableLogoutEventFlow: MutableSharedFlow<LogoutEvent> =
bufferedMutableSharedFlow()
@@ -61,10 +58,8 @@ class UserLogoutManagerImpl(
)
if (!ableToSwitchToNewAccount) {
// Update the user information and log out.
// Update the user information and log out
authDiskSource.userState = null
// Unregister the application from CXP Export since there are no other accounts.
ioScope.launch { credentialExchangeRegistryManager.unregister() }
}
clearData(userId = userId)
@@ -85,8 +80,7 @@ class UserLogoutManagerImpl(
// Save any data that will still need to be retained after otherwise clearing all dat
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
val pinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = userId)
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
switchUserIfAvailable(
currentUserId = userId,
@@ -108,9 +102,9 @@ class UserLogoutManagerImpl(
vaultTimeoutAction = vaultTimeoutAction,
)
}
authDiskSource.storePinProtectedUserKeyEnvelope(
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
pinProtectedUserKey = pinProtectedUserKey,
)
}
@@ -120,7 +114,7 @@ class UserLogoutManagerImpl(
generatorDiskSource.clearData(userId = userId)
pushDiskSource.clearData(userId = userId)
settingsDiskSource.clearData(userId = userId)
unconfinedScope.launch {
scope.launch {
passwordHistoryDiskSource.clearPasswordHistories(userId = userId)
vaultDiskSource.deleteVaultData(userId = userId)
}

View File

@@ -1,43 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import kotlinx.coroutines.flow.StateFlow
/**
* Manages the global state of all users.
*/
interface UserStateManager {
/**
* Emits updates for changes to the [UserState].
*/
val userStateFlow: StateFlow<UserState?>
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.
*
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
* Note that this call has no effect when there is no [UserState] information available.
*/
var hasPendingAccountAddition: Boolean
/**
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
*/
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
/**
* Tracks whether there is an account that is pending deletion in order to allow the account to
* remain active until the deletion is finalized.
*/
var hasPendingAccountDeletion: Boolean
/**
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
* where many individual changes might occur that would normally affect the [UserState] but we
* only want a single final emission. In the rare case that multiple threads are running
* transactions simultaneously, there will be no [UserState] updates until the last
* transaction completes.
*/
suspend fun <T> userStateTransaction(block: suspend () -> T): T
}

View File

@@ -1,176 +0,0 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
/**
* The default implementation of the [UserStateManager].
*/
class UserStateManagerImpl(
private val authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
private val policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
) : UserStateManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
//region Pending Account Addition
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
get() = mutableHasPendingAccountAdditionStateFlow
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
//endregion Pending Account Addition
//region Pending Account Deletion
/**
* If there is a pending account deletion, continue showing the original UserState until it
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
* whenever set to `true`.
*/
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
override var hasPendingAccountDeletion: Boolean
by mutableHasPendingAccountDeletionStateFlow::value
//endregion Pending Account Deletion
/**
* Whenever a function needs to update multiple underlying data-points that contribute to the
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
* until the transaction is complete. This is accomplished by blocking the emissions of the
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
* process is updating data simultaneously).
*/
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
@Suppress("UNCHECKED_CAST", "MagicNumber")
override val userStateFlow: StateFlow<UserState?> = combine(
authDiskSource.userStateFlow,
authDiskSource.userAccountTokensFlow,
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultLockManager.vaultUnlockDataStateFlow,
hasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
merge(
mutableHasPendingAccountDeletionStateFlow,
mutableUserStateTransactionCountStateFlow,
vaultLockManager.isActiveUserUnlockingFlow,
),
) { array ->
val userStateJson = array[0] as UserStateJson?
val userAccountTokens = array[1] as List<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
userOrganizationsList = userOrganizationsList,
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
hasPendingAccountAddition = hasPendingAccountAddition,
onboardingStatus = onboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
getUserPolicies = ::existingPolicies,
)
}
.filterNot {
mutableHasPendingAccountDeletionStateFlow.value ||
mutableUserStateTransactionCountStateFlow.value > 0 ||
vaultLockManager.isActiveUserUnlockingFlow.value
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = authDiskSource
.userState
?.toUserState(
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
userAccountTokens = authDiskSource.userAccountTokens,
userOrganizationsList = authDiskSource.userOrganizationsList,
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
onboardingStatus = authDiskSource.currentOnboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
getUserPolicies = ::existingPolicies,
),
)
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
mutableUserStateTransactionCountStateFlow.update { it.inc() }
return try {
block()
} finally {
mutableUserStateTransactionCountStateFlow.update { it.dec() }
}
}
private fun isBiometricsEnabled(
userId: String,
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
private fun isDeviceTrusted(
userId: String,
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = userId)
?.let { VaultUnlockType.PIN }
?: VaultUnlockType.MASTER_PASSWORD
private fun existingPolicies(
userId: String,
policyType: PolicyTypeJson,
): List<SyncResponseJson.Policy> = policyManager.getUserPolicies(
userId = userId,
type = policyType,
)
}

View File

@@ -17,8 +17,6 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManagerImpl
import com.x8bit.bitwarden.data.auth.manager.KdfManager
import com.x8bit.bitwarden.data.auth.manager.KdfManagerImpl
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
@@ -27,8 +25,6 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
@@ -121,7 +117,6 @@ object AuthManagerModule {
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
dispatcherManager: DispatcherManager,
credentialExchangeRegistryManager: CredentialExchangeRegistryManager,
): UserLogoutManager =
UserLogoutManagerImpl(
authDiskSource = authDiskSource,
@@ -133,7 +128,6 @@ object AuthManagerModule {
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,
credentialExchangeRegistryManager = credentialExchangeRegistryManager,
)
@Provides
@@ -146,18 +140,4 @@ object AuthManagerModule {
fun providesAuthTokenManager(
authDiskSource: AuthDiskSource,
): AuthTokenManager = AuthTokenManagerImpl(authDiskSource = authDiskSource)
@Provides
@Singleton
fun providesKdfManager(
authDiskSource: AuthDiskSource,
vaultSdkSource: VaultSdkSource,
accountsService: AccountsService,
featureFlagManager: FeatureFlagManager,
): KdfManager = KdfManagerImpl(
authDiskSource = authDiskSource,
vaultSdkSource = vaultSdkSource,
accountsService = accountsService,
featureFlagManager = featureFlagManager,
)
}

View File

@@ -26,7 +26,6 @@ fun TrustDeviceResponse.toUserStateJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = null,
keyConnectorUserDecryptionOptions = null,
masterPasswordUnlock = null,
)
val deviceOptions = decryptionOptions
.trustedDeviceUserDecryptionOptions

View File

@@ -6,8 +6,6 @@ import com.bitwarden.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KdfManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
@@ -29,6 +27,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
@@ -45,16 +44,17 @@ import kotlinx.coroutines.flow.StateFlow
* Provides an API for observing an modifying authentication state.
*/
@Suppress("TooManyFunctions")
interface AuthRepository :
AuthenticatorProvider,
AuthRequestManager,
KdfManager,
UserStateManager {
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Models the current auth state.
*/
val authStateFlow: StateFlow<AuthState>
/**
* Emits updates for changes to the [UserState].
*/
val userStateFlow: StateFlow<UserState?>
/**
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
@@ -110,6 +110,15 @@ interface AuthRepository :
*/
var shouldTrustDevice: Boolean
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.
*
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
* Note that this call has no effect when there is no [UserState] information available.
*/
var hasPendingAccountAddition: Boolean
/**
* Return the cached password policies for the current user.
*/
@@ -131,6 +140,11 @@ interface AuthRepository :
*/
val showWelcomeCarousel: Boolean
/**
* Clears the pending deletion state that occurs when the an account is successfully deleted.
*/
fun clearPendingAccountDeletion()
/**
* Attempt to delete the current account using the [masterPassword] and log them out
* upon success.

View File

@@ -33,8 +33,6 @@ import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.network.model.TwoFactorDataModel
import com.bitwarden.network.model.VerificationCodeResponseJson
import com.bitwarden.network.model.VerificationOtpResponseJson
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
import com.bitwarden.network.service.AccountsService
@@ -48,16 +46,15 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KdfManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
@@ -80,9 +77,13 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
@@ -90,39 +91,50 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toSdkMasterPasswordUnlock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@@ -132,7 +144,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import timber.log.Timber
import kotlinx.coroutines.flow.update
import java.time.Clock
import javax.inject.Singleton
@@ -151,7 +163,6 @@ class AuthRepositoryImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val configDiskSource: ConfigDiskSource,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
@@ -161,15 +172,12 @@ class AuthRepositoryImpl(
private val trustedDeviceManager: TrustedDeviceManager,
private val userLogoutManager: UserLogoutManager,
private val policyManager: PolicyManager,
private val userStateManager: UserStateManager,
private val kdfManager: KdfManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
pushManager: PushManager,
dispatcherManager: DispatcherManager,
) : AuthRepository,
AuthRequestManager by authRequestManager,
KdfManager by kdfManager,
UserStateManager by userStateManager {
AuthRequestManager by authRequestManager {
/**
* A scope intended for use when simply collecting multiple flows in order to combine them. The
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
@@ -182,6 +190,24 @@ class AuthRepositoryImpl(
*/
private val ioScope = CoroutineScope(dispatcherManager.io)
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
/**
* If there is a pending account deletion, continue showing the original UserState until it
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
* whenever set to `true`.
*/
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(false)
/**
* Whenever a function needs to update multiple underlying data-points that contribute to the
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
* until the transaction is complete. This is accomplished by blocking the emissions of the
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
* process is updating data simultaneously).
*/
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
/**
* The auth information to make the identity token request will need to be
* cached to make the request again in the case of two-factor authentication.
@@ -242,6 +268,68 @@ class AuthRepositoryImpl(
initialValue = AuthState.Uninitialized,
)
@Suppress("UNCHECKED_CAST", "MagicNumber")
override val userStateFlow: StateFlow<UserState?> = combine(
authDiskSource.userStateFlow,
authDiskSource.userAccountTokensFlow,
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultRepository.vaultUnlockDataStateFlow,
mutableHasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
merge(
mutableHasPendingAccountDeletionStateFlow,
mutableUserStateTransactionCountStateFlow,
vaultRepository.isActiveUserUnlockingFlow,
),
) { array ->
val userStateJson = array[0] as UserStateJson?
val userAccountTokens = array[1] as List<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
userOrganizationsList = userOrganizationsList,
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
hasPendingAccountAddition = hasPendingAccountAddition,
onboardingStatus = onboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot {
mutableHasPendingAccountDeletionStateFlow.value ||
mutableUserStateTransactionCountStateFlow.value > 0 ||
vaultRepository.isActiveUserUnlockingFlow.value
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = authDiskSource
.userState
?.toUserState(
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
userAccountTokens = authDiskSource.userAccountTokens,
userOrganizationsList = authDiskSource.userOrganizationsList,
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
onboardingStatus = authDiskSource.currentOnboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
@@ -270,6 +358,9 @@ class AuthRepositoryImpl(
}
}
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
override val passwordPolicies: List<PolicyInformation.MasterPassword>
get() = policyManager.getActivePolicies()
@@ -288,7 +379,7 @@ class AuthRepositoryImpl(
init {
combine(
userStateManager.hasPendingAccountAdditionStateFlow,
mutableHasPendingAccountAdditionStateFlow,
authDiskSource.userStateFlow,
environmentRepository.environmentStateFlow,
) { hasPendingAddition, userState, environment ->
@@ -307,21 +398,11 @@ class AuthRepositoryImpl(
.launchIn(unconfinedScope)
pushManager
.syncOrgKeysFlow
.onEach { userId ->
// This will force the next authenticated request to refresh the auth token.
authDiskSource.storeAccountTokens(
userId = userId,
accountTokens = authDiskSource
.getAccountTokens(userId = userId)
?.copy(expiresAtSec = 0L),
)
if (userId == activeUserId) {
// We just sync now to get the latest data
vaultRepository.sync(forced = true)
} else {
// We clear the last sync time to ensure we sync when we become the active user
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
}
.onEach {
val userId = activeUserId ?: return@onEach
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
refreshAccessTokenSynchronously(userId = userId)
vaultRepository.sync(forced = true)
}
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
// happens on a background thread
@@ -379,12 +460,16 @@ class AuthRepositoryImpl(
.launchIn(unconfinedScope)
}
override fun clearPendingAccountDeletion() {
mutableHasPendingAccountDeletionStateFlow.value = false
}
override suspend fun deleteAccountWithMasterPassword(
masterPassword: String,
): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
userStateManager.hasPendingAccountDeletion = true
mutableHasPendingAccountDeletionStateFlow.value = true
return authSdkSource
.hashPassword(
email = profile.email,
@@ -404,7 +489,7 @@ class AuthRepositoryImpl(
override suspend fun deleteAccountWithOneTimePassword(
oneTimePassword: String,
): DeleteAccountResult {
userStateManager.hasPendingAccountDeletion = true
mutableHasPendingAccountDeletionStateFlow.value = true
return accountsService
.deleteAccount(
masterPasswordHash = null,
@@ -416,13 +501,13 @@ class AuthRepositoryImpl(
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
fold(
onFailure = {
userStateManager.hasPendingAccountDeletion = false
clearPendingAccountDeletion()
DeleteAccountResult.Error(error = it, message = null)
},
onSuccess = { response ->
when (response) {
is DeleteAccountResponseJson.Invalid -> {
userStateManager.hasPendingAccountDeletion = false
clearPendingAccountDeletion()
DeleteAccountResult.Error(message = response.message, error = null)
}
@@ -753,17 +838,7 @@ class AuthRepositoryImpl(
?.let { jsonRequest ->
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = {
when (it) {
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
is VerificationCodeResponseJson.Invalid -> {
ResendEmailResult.Error(
message = it.firstValidationErrorMessage,
error = null,
)
}
}
},
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(
@@ -775,18 +850,8 @@ class AuthRepositoryImpl(
resendNewDeviceOtpRequestJson
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = null, error = it) },
onSuccess = {
when (it) {
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
is VerificationOtpResponseJson.Invalid -> {
ResendEmailResult.Error(
message = it.firstValidationErrorMessage,
error = null,
)
}
}
},
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(
@@ -809,7 +874,7 @@ class AuthRepositoryImpl(
// We need to make sure that the environment is set back to the correct spot.
updateEnvironment()
// No switching to do but clear any pending account additions
userStateManager.hasPendingAccountAddition = false
hasPendingAccountAddition = false
return SwitchAccountResult.NoChange
}
@@ -824,7 +889,7 @@ class AuthRepositoryImpl(
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
// Clear any pending account additions
userStateManager.hasPendingAccountAddition = false
hasPendingAccountAddition = false
return SwitchAccountResult.AccountSwitched
}
@@ -1023,6 +1088,12 @@ class AuthRepositoryImpl(
}
.fold(
onSuccess = {
// Clear the password reset reason, since it's no longer relevant.
storeUserResetPasswordReason(
userId = activeAccount.profile.userId,
reason = null,
)
// Update the saved master password hash.
authSdkSource
.hashPassword(
@@ -1038,10 +1109,6 @@ class AuthRepositoryImpl(
)
}
// Log out the user after successful password reset.
// This clears all user state including forcePasswordResetReason.
logout(reason = LogoutReason.PasswordReset)
// Return the success.
ResetPasswordResult.Success
},
@@ -1293,8 +1360,8 @@ class AuthRepositoryImpl(
?.activeAccount
?.profile
?: return ValidatePinResult.Error(error = NoActiveUserException())
val pinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = activeAccount.userId)
val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error(
error = MissingPropertyException("Pin Protected User Key"),
)
@@ -1302,7 +1369,7 @@ class AuthRepositoryImpl(
.validatePin(
userId = activeAccount.userId,
pin = pin,
pinProtectedUserKey = pinProtectedUserKeyEnvelope,
pinProtectedUserKey = pinProtectedUserKey,
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },
@@ -1485,6 +1552,27 @@ class AuthRepositoryImpl(
)
}
private fun isBiometricsEnabled(
userId: String,
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
private fun isDeviceTrusted(
userId: String,
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType =
when {
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
VaultUnlockType.PIN
}
else -> {
VaultUnlockType.MASTER_PASSWORD
}
}
/**
* Update the saved state with the force password reset reason.
*/
@@ -1595,7 +1683,7 @@ class AuthRepositoryImpl(
deviceData: DeviceDataModel?,
orgIdentifier: String?,
userConfirmedKeyConnector: Boolean,
): LoginResult = userStateManager.userStateTransaction {
): LoginResult = userStateTransaction {
val userStateJson = loginResponse.toUserState(
previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository.environment.environmentUrlData,
@@ -1634,7 +1722,7 @@ class AuthRepositoryImpl(
// we should ask him to confirm the domain
if (isNewKeyConnectorUser && isNotConfirmed) {
keyConnectorResponse = loginResponse
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
return LoginResult.ConfirmKeyConnectorDomain(
domain = keyConnectorUrl,
)
}
@@ -1685,16 +1773,6 @@ class AuthRepositoryImpl(
settingsRepository.hasUserLoggedInOrCreatedAccount = true
authDiskSource.userState = userStateJson
password?.let {
// Automatically update kdf to minimums after password unlock and userState update
kdfManager
.updateKdfToMinimumsIfNeeded(password = password)
.also { result ->
if (result is UpdateKdfMinimumsResult.Error) {
Timber.e(result.error, message = "Failed to silent update KDF settings.")
}
}
}
loginResponse.key?.let {
// Only set the value if it's present, since we may have set it already
// when we completed the pending admin auth request.
@@ -1884,27 +1962,15 @@ class AuthRepositoryImpl(
val masterPassword = password ?: return null
val privateKey = loginResponse.privateKeyOrNull() ?: return null
val key = loginResponse.key ?: return null
val initUserCryptoMethod = loginResponse
.userDecryptionOptions
?.masterPasswordUnlock
?.let { masterPasswordUnlock ->
InitUserCryptoMethod.MasterPasswordUnlock(
password = masterPassword,
masterPasswordUnlock = masterPasswordUnlock.toSdkMasterPasswordUnlock(),
)
}
?: InitUserCryptoMethod.Password(
password = masterPassword,
userKey = key,
)
return unlockVault(
accountProfile = profile,
privateKey = privateKey,
securityState = loginResponse.accountKeys?.securityState?.securityState,
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
initUserCryptoMethod = initUserCryptoMethod,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = key,
),
)
}
@@ -2090,6 +2156,22 @@ class AuthRepositoryImpl(
}
//endregion LoginCommon
/**
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
* where many individual changes might occur that would normally affect the [UserState] but we
* only want a single final emission. In the rare case that multiple threads are running
* transactions simultaneously, there will be no [UserState] updates until the last
* transaction completes.
*/
private inline fun <T> userStateTransaction(block: () -> T): T {
mutableUserStateTransactionCountStateFlow.update { it.inc() }
return try {
block()
} finally {
mutableUserStateTransactionCountStateFlow.update { it.dec() }
}
}
}
/**

View File

@@ -10,15 +10,11 @@ import com.bitwarden.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KdfManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
@@ -26,7 +22,6 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
@@ -54,7 +49,6 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
configDiskSource: ConfigDiskSource,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
@@ -66,9 +60,8 @@ object AuthRepositoryModule {
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
policyManager: PolicyManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
userStateManager: UserStateManager,
kdfManager: KdfManager,
): AuthRepository = AuthRepositoryImpl(
clock = clock,
accountsService = accountsService,
@@ -78,7 +71,6 @@ object AuthRepositoryModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
configDiskSource = configDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatcherManager,
@@ -91,24 +83,7 @@ object AuthRepositoryModule {
userLogoutManager = userLogoutManager,
pushManager = pushManager,
policyManager = policyManager,
logsManager = logsManager,
userStateManager = userStateManager,
kdfManager = kdfManager,
)
@Provides
@Singleton
fun providesUserStateManager(
authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
): UserStateManager = UserStateManagerImpl(
authDiskSource = authDiskSource,
firstTimeActionManager = firstTimeActionManager,
vaultLockManager = vaultLockManager,
policyManager = policyManager,
dispatcherManager = dispatcherManager,
logsManager = logsManager,
)
}

View File

@@ -66,11 +66,6 @@ sealed class LogoutReason {
*/
data object Notification : LogoutReason()
/**
* Indicates that the logout is happening because the user reset their master password.
*/
data object PasswordReset : LogoutReason()
/**
* Indicates that the logout is happening because the sync security stamp was invalidated.
*/

View File

@@ -122,42 +122,6 @@ sealed class PolicyInformation {
val minutes: Int?,
@SerialName("action")
val action: Action?,
@SerialName("type")
val type: Type?,
) : PolicyInformation() {
/**
* The action to take when the vault timeout is reached.
*/
@Serializable
enum class Action {
@SerialName("lock")
LOCK,
@SerialName("logOut")
LOGOUT,
}
/**
* The type of vault timeout.
*/
@Serializable
enum class Type {
@SerialName("never")
NEVER,
@SerialName("onAppRestart")
ON_APP_RESTART,
@SerialName("onSystemLock")
ON_SYSTEM_LOCK,
@SerialName("immediately")
IMMEDIATELY,
@SerialName("custom")
CUSTOM,
}
}
val action: String?,
) : PolicyInformation()
}

View File

@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
*/
data class Error(
val message: String?,
val error: Throwable?,
val error: Throwable,
) : ResendEmailResult()
}

View File

@@ -1,25 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of updating a user's kdf settings to minimums
*/
sealed class UpdateKdfMinimumsResult {
/**
* Active account was not found
*/
object ActiveAccountNotFound : UpdateKdfMinimumsResult()
/**
* There was an error updating user to minimum kdf settings.
*
* @param error the error.
*/
data class Error(
val error: Throwable?,
) : UpdateKdfMinimumsResult()
/**
* Updated user to minimum kdf settings successfully.
*/
object Success : UpdateKdfMinimumsResult()
}

View File

@@ -75,7 +75,6 @@ data class UserState(
val isUsingKeyConnector: Boolean,
val onboardingStatus: OnboardingStatus,
val firstTimeState: FirstTimeState,
val isExportable: Boolean,
) {
/**
* Indicates that the user does or does not have a means to manually unlock the vault.

View File

@@ -1,9 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import androidx.browser.auth.AuthTabIntent
import com.bitwarden.annotation.OmitFromCoverage
private const val DUO_HOST: String = "duo-callback"
@@ -19,44 +16,21 @@ private const val DUO_HOST: String = "duo-callback"
*/
fun Intent.getDuoCallbackTokenResult(): DuoCallbackTokenResult? {
val localData = data
return if (action == Intent.ACTION_VIEW && localData != null && localData.host == DUO_HOST) {
localData.getDuoCallbackTokenResult()
return if (
action == Intent.ACTION_VIEW && localData != null && localData.host == DUO_HOST
) {
val code = localData.getQueryParameter("code")
val state = localData.getQueryParameter("state")
if (code != null && state != null) {
DuoCallbackTokenResult.Success(token = "$code|$state")
} else {
DuoCallbackTokenResult.MissingToken
}
} else {
null
}
}
/**
* Retrieves a [DuoCallbackTokenResult] from an Intent. There are three possible cases.
*
* - `null`: Intent is not a Duo callback, or data is null.
*
* - [DuoCallbackTokenResult.MissingToken]: Intent is the Duo callback, but it's missing the code or
* state value.
*
* - [DuoCallbackTokenResult.Success]: Intent is the Duo callback, and it has a token.
*/
@OmitFromCoverage
fun AuthTabIntent.AuthResult.getDuoCallbackTokenResult(): DuoCallbackTokenResult =
when (this.resultCode) {
AuthTabIntent.RESULT_OK -> this.resultUri.getDuoCallbackTokenResult()
AuthTabIntent.RESULT_CANCELED -> DuoCallbackTokenResult.MissingToken
AuthTabIntent.RESULT_UNKNOWN_CODE -> DuoCallbackTokenResult.MissingToken
AuthTabIntent.RESULT_VERIFICATION_FAILED -> DuoCallbackTokenResult.MissingToken
AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> DuoCallbackTokenResult.MissingToken
else -> DuoCallbackTokenResult.MissingToken
}
private fun Uri?.getDuoCallbackTokenResult(): DuoCallbackTokenResult {
val code = this?.getQueryParameter("code")
val state = this?.getQueryParameter("state")
return if (code != null && state != null) {
DuoCallbackTokenResult.Success(token = "$code|$state")
} else {
DuoCallbackTokenResult.MissingToken
}
}
/**
* Sealed class representing the result of Duo callback token extraction.
*/

View File

@@ -1,10 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.browser.auth.AuthTabIntent
import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.parcelize.Parcelize
import java.net.URLEncoder
import java.security.MessageDigest
@@ -64,40 +61,21 @@ fun generateUriForSso(
fun Intent.getSsoCallbackResult(): SsoCallbackResult? {
val localData = data
return if (action == Intent.ACTION_VIEW && localData?.host == SSO_HOST) {
localData.getSsoCallbackResult()
val state = localData.getQueryParameter("state")
val code = localData.getQueryParameter("code")
if (code != null) {
SsoCallbackResult.Success(
state = state,
code = code,
)
} else {
SsoCallbackResult.MissingCode
}
} else {
null
}
}
/**
* Retrieves an [SsoCallbackResult] from an [AuthTabIntent.AuthResult]. There are two possible
* cases.
*
* - [SsoCallbackResult.MissingCode]: The code is missing.
* - [SsoCallbackResult.Success]: The relevant data is present.
*/
@OmitFromCoverage
fun AuthTabIntent.AuthResult.getSsoCallbackResult(): SsoCallbackResult =
when (this.resultCode) {
AuthTabIntent.RESULT_OK -> this.resultUri.getSsoCallbackResult()
AuthTabIntent.RESULT_CANCELED -> SsoCallbackResult.MissingCode
AuthTabIntent.RESULT_UNKNOWN_CODE -> SsoCallbackResult.MissingCode
AuthTabIntent.RESULT_VERIFICATION_FAILED -> SsoCallbackResult.MissingCode
AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> SsoCallbackResult.MissingCode
else -> SsoCallbackResult.MissingCode
}
private fun Uri?.getSsoCallbackResult(): SsoCallbackResult {
val state = this?.getQueryParameter("state")
val code = this?.getQueryParameter("code")
return if (code != null) {
SsoCallbackResult.Success(state = state, code = code)
} else {
SsoCallbackResult.MissingCode
}
}
/**
* Sealed class representing the result of an SSO callback data extraction.
*/

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
@@ -14,7 +12,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
@@ -35,7 +32,6 @@ fun UserStateJson.toRemovedPasswordUserStateJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = null,
keyConnectorUserDecryptionOptions = null,
masterPasswordUnlock = null,
)
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
val updatedAccount = account.copy(profile = updatedProfile)
@@ -58,23 +54,6 @@ fun UserStateJson.toUpdatedUserStateJson(
val userId = syncProfile.id
val account = this.accounts[userId] ?: return this
val profile = account.profile
val userDecryptionOptions = syncResponse
.userDecryption
?.let { syncUserDecryption ->
profile
.userDecryptionOptions
?.copy(masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock)
?: UserDecryptionOptionsJson(
hasMasterPassword = syncUserDecryption.masterPasswordUnlock != null,
trustedDeviceUserDecryptionOptions = null,
keyConnectorUserDecryptionOptions = null,
masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock,
)
}
?: profile
.userDecryptionOptions
?.copy(masterPasswordUnlock = null)
val updatedProfile = profile
.copy(
avatarColorHex = syncProfile.avatarColor,
@@ -82,7 +61,6 @@ fun UserStateJson.toUpdatedUserStateJson(
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
creationDate = syncProfile.creationDate,
userDecryptionOptions = userDecryptionOptions,
)
val updatedAccount = account.copy(profile = updatedProfile)
return this
@@ -112,7 +90,6 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
hasMasterPassword = true,
keyConnectorUserDecryptionOptions = null,
trustedDeviceUserDecryptionOptions = null,
masterPasswordUnlock = null,
),
)
val updatedAccount = account.copy(profile = updatedProfile)
@@ -126,30 +103,6 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
)
}
/**
* Updates the [UserStateJson] KDF settings to minimum requirements.
*/
fun UserStateJson.toUserStateJsonKdfUpdatedMinimums(): UserStateJson {
val account = this.activeAccount
val profile = account.profile
val updatedProfile = profile
.copy(
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
kdfMemory = null,
kdfParallelism = null,
)
val updatedAccount = account.copy(profile = updatedProfile)
return this
.copy(
accounts = accounts
.toMutableMap()
.apply {
replace(activeUserId, updatedAccount)
},
)
}
/**
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
*/
@@ -165,7 +118,6 @@ fun UserStateJson.toUserState(
isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
getUserPolicies: (userId: String, policy: PolicyTypeJson) -> List<SyncResponseJson.Policy>,
): UserState =
UserState(
activeUserId = this.activeUserId,
@@ -205,19 +157,6 @@ fun UserStateJson.toUserState(
hasManageResetPasswordPermission.takeIf { trustedDevice != null }
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
(tdeUserNeedsMasterPassword ?: (keyConnectorOptions == null))
val hasPersonalOwnershipRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.PERSONAL_OWNERSHIP,
)
.any { it.isEnabled }
val hasPersonalVaultExportRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
)
.any { it.isEnabled }
UserState.Account(
userId = userId,
name = profile.name,
@@ -246,8 +185,6 @@ fun UserStateJson.toUserState(
// using the app prior to the release of the onboarding flow.
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
firstTimeState = firstTimeState,
isExportable = !hasPersonalOwnershipRestrictedOrg &&
!hasPersonalVaultExportRestrictedOrg,
)
},
hasPendingAccountAddition = hasPendingAccountAddition,

View File

@@ -2,9 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import androidx.browser.auth.AuthTabIntent
import androidx.core.net.toUri
import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
@@ -27,36 +24,15 @@ fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
localData != null &&
localData.host == WEB_AUTH_HOST
) {
localData.getWebAuthResult()
localData
.getQueryParameter("data")
?.let { WebAuthResult.Success(token = it) }
?: WebAuthResult.Failure(message = localData.getQueryParameter("error"))
} else {
null
}
}
/**
* Retrieves an [WebAuthResult] from an [AuthTabIntent.AuthResult]. There are two possible cases.
*
* - [WebAuthResult.Success]: The URI is the web auth key callback with correct data.
* - [WebAuthResult.Failure]: The URI is the web auth key callback with incorrect data or a failure
* has occurred.
*/
@OmitFromCoverage
fun AuthTabIntent.AuthResult.getWebAuthResult(): WebAuthResult =
when (this.resultCode) {
AuthTabIntent.RESULT_OK -> this.resultUri.getWebAuthResult()
AuthTabIntent.RESULT_CANCELED -> WebAuthResult.Failure(message = null)
AuthTabIntent.RESULT_UNKNOWN_CODE -> WebAuthResult.Failure(message = null)
AuthTabIntent.RESULT_VERIFICATION_FAILED -> WebAuthResult.Failure(message = null)
AuthTabIntent.RESULT_VERIFICATION_TIMED_OUT -> WebAuthResult.Failure(message = null)
else -> WebAuthResult.Failure(message = null)
}
private fun Uri?.getWebAuthResult(): WebAuthResult =
this
?.getQueryParameter("data")
?.let { WebAuthResult.Success(token = it) }
?: WebAuthResult.Failure(message = this?.getQueryParameter("error"))
/**
* Generates a [Uri] to display a web authn challenge for Bitwarden authentication.
*/
@@ -83,7 +59,7 @@ fun generateUriForWebAuth(
"?data=$base64Data" +
"&parent=$parentParam" +
"&v=2"
return url.toUri()
return Uri.parse(url)
}
/**

View File

@@ -136,11 +136,6 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
),
Browser(
packageName = "org.ironfoxoss.ironfox.nightly",
// 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(

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
import com.x8bit.bitwarden.data.autofill.util.buildUri
import timber.log.Timber
/**
@@ -84,7 +83,6 @@ class FilledDataBuilderImpl(
autofillCipher = autofillCipher,
autofillViews = autofillRequest.partition.views,
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
packageName = autofillRequest.packageName,
)
}
}
@@ -98,9 +96,7 @@ class FilledDataBuilderImpl(
?.getOrLastOrNull(inlineSuggestionsAdded)
return FilledData(
filledPartitions = filledPartitions
.filter { it.filledItems.isNotEmpty() }
.take(n = MAX_FILLED_PARTITIONS_COUNT),
filledPartitions = filledPartitions.take(n = MAX_FILLED_PARTITIONS_COUNT),
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
originalPartition = autofillRequest.partition,
uri = autofillRequest.uri,
@@ -144,21 +140,16 @@ class FilledDataBuilderImpl(
autofillCipher: AutofillCipher.Login,
autofillViews: List<AutofillView.Login>,
inlinePresentationSpec: InlinePresentationSpec?,
packageName: String?,
): FilledPartition {
val filledItems = autofillViews
.mapNotNull { autofillView ->
if (autofillView.data.website == autofillCipher.website ||
buildUri(packageName.orEmpty(), "androidapp") == autofillCipher.website
) {
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
}
autofillView.buildFilledItemOrNull(value = value)
} else {
null
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
}
autofillView.buildFilledItemOrNull(
value = value,
)
}
return FilledPartition(

View File

@@ -16,8 +16,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
@@ -26,8 +24,6 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -65,22 +61,6 @@ object AutofillModule {
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
BrowserThirdPartyAutofillEnabledManagerImpl()
@Singleton
@Provides
fun providesBrowserAutofillDialogManager(
autofillEnabledManager: AutofillEnabledManager,
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
clock: Clock,
firstTimeActionManager: FirstTimeActionManager,
settingsDiskSource: SettingsDiskSource,
): BrowserAutofillDialogManager = BrowserAutofillDialogManagerImpl(
autofillEnabledManager = autofillEnabledManager,
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
clock = clock,
firstTimeActionManager = firstTimeActionManager,
settingsDiskSource = settingsDiskSource,
)
@Singleton
@Provides
fun provideAutofillCompletionManager(

View File

@@ -1,21 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
/**
* Manager to handle whether the Browser Autofill Dialog should be displayed.
*/
interface BrowserAutofillDialogManager {
/**
* Number of browsers installed that may need autofill enabled.
*/
val browserCount: Int
/**
* Indicates whether the dialog should be displayed to the user.
*/
val shouldShowDialog: Boolean
/**
* The dialog has been dismissed and we should delay displaying it again.
*/
fun delayDialog()
}

View File

@@ -1,42 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import java.time.Clock
/**
* We only show the dialog once per 24 hour period.
*/
private const val SHOW_DIALOG_DELAY_MS: Long = 24L * 60L * 60L * 1000L
/**
* The default implementation of the [BrowserAutofillDialogManager].
*/
internal class BrowserAutofillDialogManagerImpl(
private val autofillEnabledManager: AutofillEnabledManager,
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
private val clock: Clock,
private val firstTimeActionManager: FirstTimeActionManager,
private val settingsDiskSource: SettingsDiskSource,
) : BrowserAutofillDialogManager {
override val browserCount: Int
get() = browserThirdPartyAutofillEnabledManager
.browserThirdPartyAutofillStatus
.availableCount
override val shouldShowDialog: Boolean
get() = autofillEnabledManager.isAutofillEnabled &&
browserThirdPartyAutofillEnabledManager
.browserThirdPartyAutofillStatus
.isAnyIsAvailableAndDisabled &&
!firstTimeActionManager
.currentOrDefaultUserFirstTimeState
.showSetupBrowserAutofillCard &&
settingsDiskSource.browserAutofillDialogReshowTime?.isBefore(clock.instant()) != false
override fun delayDialog() {
settingsDiskSource.browserAutofillDialogReshowTime =
clock.instant().plusMillis(SHOW_DIALOG_DELAY_MS)
}
}

View File

@@ -66,7 +66,6 @@ sealed class AutofillCipher {
override val subtitle: String,
val password: String,
val username: String,
val website: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = BitwardenDrawable.ic_globe

View File

@@ -16,7 +16,6 @@ sealed class AutofillView {
* @param isFocused Whether the view is currently focused.
* @param textValue A text value that represents the input present in the field.
* @param hasPasswordTerms Indicates that the field includes password terms.
* @param website website associated with this view.
*/
data class Data(
val autofillId: AutofillId,
@@ -25,7 +24,6 @@ sealed class AutofillView {
val isFocused: Boolean,
val textValue: String?,
val hasPasswordTerms: Boolean,
val website: String?,
)
/**

View File

@@ -7,12 +7,12 @@ import android.view.autofill.AutofillId
*
* @param autofillViews The list of views we care about for autofilling.
* @param idPackage The package id for this view, if there is one.
* @param urlBarWebsites The website associated with the URL bar view.
* @param ignoreAutofillIds The list of [AutofillId]s that should be ignored in the fill response.
* @param website The website that is being displayed in the app, given there is one.
*/
data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val idPackage: String?,
val urlBarWebsites: List<String>,
val ignoreAutofillIds: List<AutofillId>,
val website: String?,
)

View File

@@ -6,9 +6,7 @@ package com.x8bit.bitwarden.data.autofill.model.browser
data class BrowserThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
) {
val isAvailableButDisabled: Boolean = isAvailable && !isThirdPartyEnabled
}
)
/**
* The overall status for all relevant browsers.
@@ -17,20 +15,4 @@ data class BrowserThirdPartyAutofillStatus(
val braveStableStatusData: BrowserThirdPartyAutoFillData,
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
) {
/**
* The total number of available browsers.
*/
val availableCount: Int
get() = (if (braveStableStatusData.isAvailable) 1 else 0) +
(if (chromeStableStatusData.isAvailable) 1 else 0) +
(if (chromeBetaChannelStatusData.isAvailable) 1 else 0)
/**
* Whether any of the available browsers have third party autofill disabled.
*/
val isAnyIsAvailableAndDisabled: Boolean
get() = braveStableStatusData.isAvailableButDisabled ||
chromeStableStatusData.isAvailableButDisabled ||
chromeBetaChannelStatusData.isAvailableButDisabled
}
)

View File

@@ -29,20 +29,6 @@ private val BLOCK_LISTED_URIS: List<String> = listOf(
"androidapp://com.oneplus.applocker",
)
/**
* A map of package ids and the known associated id entry for their url bar.
*/
private val URL_BARS: Map<String, String> = mapOf(
"com.microsoft.emmx" to "url_bar",
"com.microsoft.emmx.beta" to "url_bar",
"com.microsoft.emmx.canary" to "url_bar",
"com.microsoft.emmx.dev" to "url_bar",
"com.sec.android.app.sbrowser" to "location_bar_edit_text",
"com.sec.android.app.sbrowser.beta" to "location_bar_edit_text",
"com.opera.browser" to "url_bar",
"com.opera.browser.beta" to "url_bar",
)
/**
* The default [AutofillParser] implementation for the app. This is a tool for parsing autofill data
* from the OS into domain models.
@@ -90,24 +76,18 @@ class AutofillParserImpl(
Timber.d("Parsing AssistStructure -- ${fillRequest?.id}")
// Parse the `assistStructure` into internal models.
val traversalDataList = assistStructure.traverse()
val urlBarWebsite = traversalDataList
.flatMap { it.urlBarWebsites }
.firstOrNull()
?.takeIf { settingsRepository.isAutofillWebDomainCompatMode }
// Take only the autofill views from the node that currently has focus.
// Then remove all the fields that cannot be filled with data.
// We fallback to taking all the fillable views if nothing has focus.
val autofillViewsList = traversalDataList.map { it.autofillViews }
val autofillViews = (autofillViewsList
val autofillViews = autofillViewsList
.filter { views -> views.any { it.data.isFocused } }
.flatten()
.filter { it !is AutofillView.Unused }
.takeUnless { it.isEmpty() }
?: autofillViewsList
.flatten()
.filter { it !is AutofillView.Unused })
.map { it.updateWebsiteIfNecessary(website = urlBarWebsite) }
.filter { it !is AutofillView.Unused }
// Find the focused view, or fallback to the first fillable item on the screen (so
// we at least have something to hook into)
@@ -115,21 +95,16 @@ class AutofillParserImpl(
.firstOrNull { it.data.isFocused }
?: autofillViews.firstOrNull()
if (focusedView == null) {
// The view is unfillable if there are no focused views.
return AutofillRequest.Unfillable
}
val packageName = traversalDataList.buildPackageNameOrNull(
assistStructure = assistStructure,
)
val uri = focusedView.buildUriOrNull(
val uri = traversalDataList.buildUriOrNull(
packageName = packageName,
)
val blockListedURIs = settingsRepository.blockedAutofillUris + BLOCK_LISTED_URIS
if (blockListedURIs.contains(uri)) {
// The view is unfillable if the URI is block listed.
if (focusedView == null || blockListedURIs.contains(uri)) {
// The view is unfillable if there are no focused views or the URI is block listed.
return AutofillRequest.Unfillable
}
@@ -190,7 +165,7 @@ private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
.mapNotNull { windowNode ->
windowNode
.rootViewNode
?.traverse(parentWebsite = null)
?.traverse()
?.updateForMissingPasswordFields()
?.updateForMissingUsernameFields()
}
@@ -268,25 +243,16 @@ private fun ViewNodeTraversalData.copyAndMapAutofillViews(
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
* data into [ViewNodeTraversalData].
*/
private fun AssistStructure.ViewNode.traverse(
parentWebsite: String?,
): ViewNodeTraversalData {
private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
// Set up mutable lists for collecting valid AutofillViews and ignorable view ids.
val mutableAutofillViewList: MutableList<AutofillView> = mutableListOf()
val mutableIgnoreAutofillIdList: MutableList<AutofillId> = mutableListOf()
// OS sometimes defaults node.idPackage to "android", which is not a valid
// package name so it is ignored to prevent auto-filling unknown applications.
var storedIdPackage: String? = this.idPackage?.takeUnless { it.isBlank() || it == "android" }
val storedUrlBarId = storedIdPackage?.let { URL_BARS[it] }
val storedUrlBarWebsites: MutableList<String> = this
.website
?.takeIf { _ -> storedUrlBarId != null && storedUrlBarId == this.idEntry }
?.let { mutableListOf(it) }
?: mutableListOf()
var idPackage: String? = this.idPackage
var website: String? = this.website
// Try converting this `ViewNode` into an `AutofillView`. If a valid instance is returned, add
// it to the list. Otherwise, ignore the `AutofillId` associated with this `ViewNode`.
toAutofillView(parentWebsite = parentWebsite)
toAutofillView()
?.run(mutableAutofillViewList::add)
?: autofillId?.run(mutableIgnoreAutofillIdList::add)
@@ -294,18 +260,23 @@ private fun AssistStructure.ViewNode.traverse(
for (i in 0 until childCount) {
// Extract the traversal data from each child view node and add it to the lists.
getChildAt(i)
.traverse(parentWebsite = website)
.traverse()
.let { viewNodeTraversalData ->
viewNodeTraversalData.autofillViews.forEach(mutableAutofillViewList::add)
viewNodeTraversalData.ignoreAutofillIds.forEach(mutableIgnoreAutofillIdList::add)
// Get the first non-null idPackage.
if (storedIdPackage == null) {
storedIdPackage = viewNodeTraversalData.idPackage
if (idPackage.isNullOrBlank() &&
// OS sometimes defaults node.idPackage to "android", which is not a valid
// package name so it is ignored to prevent auto-filling unknown applications.
viewNodeTraversalData.idPackage?.equals("android") == false
) {
idPackage = viewNodeTraversalData.idPackage
}
// Get the first non-null website.
if (website == null) {
website = viewNodeTraversalData.website
}
// Add all url bar websites. We will deal with this later if
// there is somehow more than one.
storedUrlBarWebsites.addAll(viewNodeTraversalData.urlBarWebsites)
}
}
@@ -313,29 +284,8 @@ private fun AssistStructure.ViewNode.traverse(
// descendant's.
return ViewNodeTraversalData(
autofillViews = mutableAutofillViewList,
idPackage = storedIdPackage,
urlBarWebsites = storedUrlBarWebsites,
idPackage = idPackage,
ignoreAutofillIds = mutableIgnoreAutofillIdList,
website = website,
)
}
/**
* This updates the underlying [AutofillView.data] with the given [website] if it does not already
* have a website associated with it.
*/
private fun AutofillView.updateWebsiteIfNecessary(website: String?): AutofillView {
val site = website ?: return this
if (this.data.website != null) return this
return when (this) {
is AutofillView.Card.Brand -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.CardholderName -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.ExpirationDate -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.ExpirationMonth -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.ExpirationYear -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.Number -> this.copy(data = this.data.copy(website = site))
is AutofillView.Card.SecurityCode -> this.copy(data = this.data.copy(website = site))
is AutofillView.Login.Password -> this.copy(data = this.data.copy(website = site))
is AutofillView.Login.Username -> this.copy(data = this.data.copy(website = site))
is AutofillView.Unused -> this.copy(data = this.data.copy(website = site))
}
}

View File

@@ -127,7 +127,6 @@ class AutofillCipherProviderImpl(
password = cipherView.login?.password.orEmpty(),
subtitle = cipherView.subtitle.orEmpty(),
username = cipherView.login?.username.orEmpty(),
website = uri,
)
}
}

View File

@@ -4,15 +4,9 @@ import android.view.View
import android.view.autofill.AutofillValue
import com.x8bit.bitwarden.data.autofill.model.AutofillView
import com.x8bit.bitwarden.data.autofill.model.FilledItem
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"
/**
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
*/
@@ -102,17 +96,3 @@ private fun AutofillView.buildListAutofillValueOrNull(
?.let { AutofillValue.forList(it) }
}
}
/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun AutofillView.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this.data.website?.let { websiteUri -> return websiteUri }
// If the package name is available, build a URI out of that.
return packageName?.let { buildUri(domain = it, scheme = ANDROID_APP_SCHEME) }
}

View File

@@ -41,7 +41,6 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
password = login.password.orEmpty(),
subtitle = subtitle.orEmpty(),
username = login.username.orEmpty(),
website = uri,
),
)
}

View File

@@ -49,30 +49,34 @@ private val AssistStructure.ViewNode.isInputField: Boolean
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
* it doesn't have a supported hint and isn't an input field, we also return null.
*/
fun AssistStructure.ViewNode.toAutofillView(
parentWebsite: String?,
): AutofillView? {
val nonNullAutofillId = this.autofillId ?: return null
if (this.supportedAutofillHint == null && !this.isInputField) return null
val autofillOptions = this
.autofillOptions
.orEmpty()
.map { it.toString() }
val autofillViewData = AutofillView.Data(
autofillId = nonNullAutofillId,
autofillOptions = autofillOptions,
autofillType = this.autofillType,
isFocused = this.isFocused,
textValue = this.autofillValue?.extractTextValue(),
hasPasswordTerms = this.hasPasswordTerms(),
website = this.website ?: parentWebsite,
)
return buildAutofillView(
autofillOptions = autofillOptions,
autofillViewData = autofillViewData,
autofillHint = this.supportedAutofillHint,
)
}
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
this
.autofillId
// We only care about nodes with a valid `AutofillId`.
?.let { nonNullAutofillId ->
if (supportedAutofillHint != null || this.isInputField) {
val autofillOptions = this
.autofillOptions
.orEmpty()
.map { it.toString() }
val autofillViewData = AutofillView.Data(
autofillId = nonNullAutofillId,
autofillOptions = autofillOptions,
autofillType = this.autofillType,
isFocused = this.isFocused,
textValue = this.autofillValue?.extractTextValue(),
hasPasswordTerms = this.hasPasswordTerms(),
)
buildAutofillView(
autofillOptions = autofillOptions,
autofillViewData = autofillViewData,
autofillHint = supportedAutofillHint,
)
} else {
null
}
}
/**
* The first supported autofill hint for this view node, or null if none are found.

View File

@@ -4,6 +4,36 @@ import android.app.assist.AssistStructure
import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"
/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun List<ViewNodeTraversalData>.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this
.firstOrNull { it.website != null }
?.website
?.let { websiteUri ->
return websiteUri
}
// If the package name is available, build a URI out of that.
return packageName
?.let { nonNullPackageName ->
buildUri(
domain = nonNullPackageName,
scheme = ANDROID_APP_SCHEME,
)
}
}
/**
* Try and build a package name. First, try searching traversal data for package names. If that
* fails, try extracting a package name from [assistStructure].

View File

@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -92,11 +93,13 @@ object CredentialProviderModule {
assetManager: AssetManager,
digitalAssetLinkService: DigitalAssetLinkService,
privilegedAppRepository: PrivilegedAppRepository,
featureFlagManager: FeatureFlagManager,
): OriginManager =
OriginManagerImpl(
assetManager = assetManager,
digitalAssetLinkService = digitalAssetLinkService,
privilegedAppRepository = privilegedAppRepository,
featureFlagManager = featureFlagManager,
)
@Provides

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.credentials.manager
import android.util.Base64
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.exceptions.GetCredentialUnknownException
@@ -16,7 +15,6 @@ import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
@@ -26,6 +24,7 @@ import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.util.isActiveWithCopyablePassword
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.autofill.util.login
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
@@ -42,6 +41,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.fold
@@ -207,6 +207,8 @@ class BitwardenCredentialManagerImpl(
.beginGetPublicKeyCredentialOptions
.toPublicKeyCredentialEntries(
userId = getCredentialsRequest.userId,
cipherListViews = cipherListViews
.filter { it.isActiveWithFido2Credentials },
)
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
@@ -223,72 +225,65 @@ class BitwardenCredentialManagerImpl(
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
userId: String,
cipherListViews: List<CipherListView>,
): Result<List<CredentialEntry>> {
if (this.isEmpty()) return emptyList<CredentialEntry>().asSuccess()
val assertionOptions = this
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson) }
.ifEmpty {
return GetCredentialUnknownException(
"Passkey assertion options required.",
)
.asFailure()
}
val relyingPartyIds = assertionOptions
.mapNotNull { it.relyingPartyId }
.toSet()
val relyingPartyIds = this
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
.distinct()
.ifEmpty {
return GetCredentialUnknownException("Relying party id required.").asFailure()
}
val allowedCredentials = assertionOptions
.flatMap { option ->
option
.allowCredentials
?.map { it.id }
val cipherViews = cipherListViews
.filter { cipherListView ->
cipherListView.login
?.fido2Credentials
.orEmpty()
.any { credential -> credential.rpId in relyingPartyIds }
}
.mapNotNull { cipherListView ->
when (val result = vaultRepository.getCipher(cipherListView.id.orEmpty())) {
GetCipherResult.CipherNotFound -> {
Timber.e("Cipher not found while building public key credential entries.")
null
}
val discoveredCredentials = relyingPartyIds
.flatMap { relyingPartyId ->
vaultSdkSource
.silentlyDiscoverCredentials(
userId = userId,
fido2CredentialStore = fido2CredentialStore,
relyingPartyId = relyingPartyId,
)
.fold(
onSuccess = { it },
onFailure = {
Timber.e(it, "Failed to discover credentials.")
emptyList()
},
)
is GetCipherResult.Failure -> {
Timber.e(
result.error,
"Failed to decrypt cipher while building credential entries.",
)
null
}
is GetCipherResult.Success -> result.cipherView
}
}
.filterAllowedCredentialsIfNecessary(allowedCredentials)
.toTypedArray()
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
return credentialEntryBuilder
.buildPublicKeyCredentialEntries(
return vaultSdkSource
.decryptFido2CredentialAutofillViews(
userId = userId,
fido2CredentialAutofillViews = discoveredCredentials,
beginGetPublicKeyCredentialOptions = this,
isUserVerified = isUserVerified,
cipherViews = cipherViews,
)
.fold(
onSuccess = { fido2AutofillViews ->
credentialEntryBuilder
.buildPublicKeyCredentialEntries(
userId = userId,
fido2CredentialAutofillViews = fido2AutofillViews,
beginGetPublicKeyCredentialOptions = this,
isUserVerified = isUserVerified,
)
.asSuccess()
},
onFailure = {
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
},
)
.asSuccess()
}
private fun List<Fido2CredentialAutofillView>.filterAllowedCredentialsIfNecessary(
allowedCredentialIds: List<String>,
): List<Fido2CredentialAutofillView> = if (allowedCredentialIds.isEmpty()) {
this
} else {
this.filter {
Base64
.encodeToString(
it.credentialId,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
) in allowedCredentialIds
}
}
private suspend fun registerFido2CredentialForUnprivilegedApp(
@@ -323,7 +318,6 @@ class BitwardenCredentialManagerImpl(
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
callingPackageName = callingAppInfo.packageName,
)
}
@@ -348,7 +342,6 @@ class BitwardenCredentialManagerImpl(
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
callingPackageName = callingAppInfo.packageName,
)
}
@@ -358,7 +351,6 @@ class BitwardenCredentialManagerImpl(
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
clientData: ClientData,
callingPackageName: String,
): Fido2RegisterCredentialResult = vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
@@ -373,9 +365,7 @@ class BitwardenCredentialManagerImpl(
),
fido2CredentialStore = this,
)
.map {
it.toAndroidAttestationResponse(callingPackageName = callingPackageName)
}
.map { it.toAndroidAttestationResponse() }
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },

View File

@@ -1,11 +1,13 @@
package com.x8bit.bitwarden.data.credentials.manager
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.network.service.DigitalAssetLinkService
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
import timber.log.Timber
@@ -21,6 +23,7 @@ class OriginManagerImpl(
private val assetManager: AssetManager,
private val digitalAssetLinkService: DigitalAssetLinkService,
private val privilegedAppRepository: PrivilegedAppRepository,
private val featureFlagManager: FeatureFlagManager,
) : OriginManager {
override suspend fun validateOrigin(
@@ -67,7 +70,10 @@ class OriginManagerImpl(
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
?: validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
.takeUnless {
it is ValidateOriginResult.Error.PrivilegedAppNotAllowed &&
featureFlagManager.getFeatureFlag(FlagKey.UserManagedPrivilegedApps)
}
?: validatePrivilegedAppSignatureWithUserTrustList(callingAppInfo)
private suspend fun validatePrivilegedAppSignatureWithGoogleList(

View File

@@ -20,7 +20,7 @@ data class Fido2AttestationResponse(
@SerialName("response")
val response: RegistrationResponse,
@SerialName("clientExtensionResults")
val clientExtensionResults: ClientExtensionResults,
val clientExtensionResults: ClientExtensionResults?,
@SerialName("authenticatorAttachment")
val authenticatorAttachment: String?,
) {
@@ -50,7 +50,7 @@ data class Fido2AttestationResponse(
@Serializable
data class ClientExtensionResults(
@SerialName("credProps")
val credentialProperties: CredentialProperties? = null,
val credentialProperties: CredentialProperties,
) {
/**
* Represents properties for newly created credential.

View File

@@ -105,16 +105,6 @@ interface SettingsDiskSource {
*/
val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
/**
* The time at which the browser autofill dialog is allowed to be shown to the user again.
*/
var browserAutofillDialogReshowTime: Instant?
/**
* The current status of whether the web domain compatibility mode is enabled.
*/
var isAutofillWebDomainCompatMode: Boolean?
/**
* Clears all the settings data for the given user.
*/
@@ -291,23 +281,6 @@ interface SettingsDiskSource {
*/
fun getUserHasSignedInPreviously(userId: String): Boolean
/**
* Gets whether or not the given [userId] has signalled they want to enable autofill in
* onboarding.
*/
fun getShowBrowserAutofillSettingBadge(userId: String): Boolean?
/**
* Stores the given value for whether or not the given [userId] has signalled they want to
* enable the browser autofill integration in onboarding.
*/
fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?)
/**
* Emits updates that track [getShowAutoFillSettingBadge] for the given [userId].
*/
fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the given [userId] has signalled they want to enable autofill in
* onboarding.
@@ -359,21 +332,21 @@ interface SettingsDiskSource {
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the application has registered for export via the credential exchange
* Gets whether or not the given [userId] has registered for export via the credential exchange
* protocol.
*/
fun getAppRegisteredForExport(): Boolean?
fun getVaultRegisteredForExport(userId: String): Boolean?
/**
* Stores the given value for whether or not the application has registered for export via
* Stores the given value for whether or not the given [userId] has registered for export via
* the credential exchange protocol.
*/
fun storeAppRegisteredForExport(isRegistered: Boolean?)
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?)
/**
* Emits updates that track [getAppRegisteredForExport].
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
*/
fun getAppRegisteredForExportFlow(userId: String): Flow<Boolean?>
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
/**
* Gets the number of qualifying add cipher actions for the device.

View File

@@ -37,7 +37,6 @@ private const val CLEAR_CLIPBOARD_INTERVAL_KEY = "clearClipboard"
private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
private const val SHOW_BROWSER_AUTOFILL_SETTING_BADGE = "showBrowserAutofillSettingBadge"
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
@@ -49,8 +48,6 @@ private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMa
private const val RESUME_SCREEN = "resumeScreen"
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
private const val AUTOFILL_WEB_DOMAIN_COMPATIBILITY = "autofillWebDomainCompatibility"
/**
* Primary implementation of [SettingsDiskSource].
@@ -75,9 +72,6 @@ class SettingsDiskSourceImpl(
private val mutablePullToRefreshEnabledFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowBrowserAutofillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowAutoFillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@@ -101,7 +95,8 @@ class SettingsDiskSourceImpl(
private val mutableScreenCaptureAllowedFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableVaultRegisteredForExportFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableVaultRegisteredForExportFlow =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableIsDynamicColorsEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
@@ -229,18 +224,6 @@ class SettingsDiskSourceImpl(
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) }
override var browserAutofillDialogReshowTime: Instant?
get() = getLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME)?.let { Instant.ofEpochMilli(it) }
set(value) {
putLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME, value = value?.toEpochMilli())
}
override var isAutofillWebDomainCompatMode: Boolean?
get() = getBoolean(key = AUTOFILL_WEB_DOMAIN_COMPATIBILITY)
set(value) {
putBoolean(key = AUTOFILL_WEB_DOMAIN_COMPATIBILITY, value = value)
}
override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
@@ -253,6 +236,7 @@ class SettingsDiskSourceImpl(
storeLastSyncTime(userId = userId, lastSyncTime = null)
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
storeAppResumeScreen(userId = userId, screenData = null)
// The following are intentionally not cleared so they can be
@@ -447,21 +431,6 @@ class SettingsDiskSourceImpl(
key = HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY.appendIdentifier(userId),
) == true
override fun getShowBrowserAutofillSettingBadge(userId: String): Boolean? =
getBoolean(key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId))
override fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?) {
putBoolean(
key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
value = showBadge,
)
getMutableShowBrowserAutofillSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowBrowserAutofillSettingBadgeFlow(userId = userId)
.onSubscription { emit(getShowBrowserAutofillSettingBadge(userId)) }
override fun getShowAutoFillSettingBadge(userId: String): Boolean? =
getBoolean(
key = SHOW_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
@@ -514,17 +483,17 @@ class SettingsDiskSourceImpl(
getMutableShowImportLoginsSettingBadgeFlow(userId)
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
override fun getAppRegisteredForExport(): Boolean? =
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT)
override fun getVaultRegisteredForExport(userId: String): Boolean? =
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId))
override fun storeAppRegisteredForExport(isRegistered: Boolean?) {
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT, isRegistered)
mutableVaultRegisteredForExportFlow.tryEmit(isRegistered)
override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?) {
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId), isRegistered)
getMutableVaultRegisteredForExportFlow(userId).tryEmit(isRegistered)
}
override fun getAppRegisteredForExportFlow(userId: String): Flow<Boolean?> =
mutableVaultRegisteredForExportFlow
.onSubscription { emit(getAppRegisteredForExport()) }
override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
getMutableVaultRegisteredForExportFlow(userId)
.onSubscription { emit(getVaultRegisteredForExport(userId)) }
override fun getAddCipherActionCount(): Int? = getInt(
key = ADD_ACTION_COUNT,
@@ -629,13 +598,6 @@ class SettingsDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowBrowserAutofillSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> =
mutableShowBrowserAutofillSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowAutoFillSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
@@ -654,6 +616,12 @@ class SettingsDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultRegisteredForExportFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableVaultRegisteredForExportFlow.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
/**
* Migrates the user-scoped screen capture state to an app-wide state.
*

View File

@@ -1,6 +0,0 @@
package com.x8bit.bitwarden.data.platform.error
/**
* An exception indicating that the security stamps for the current user do not match.
*/
class SecurityStampMismatchException : IllegalStateException("Security stamps do not match!")

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.RegisterExportResult
import com.x8bit.bitwarden.data.platform.manager.model.UnregisterExportResult
/**
* Manager for registering for Credential Exchange Protocol export.
*/
interface CredentialExchangeRegistryManager {
/**
* Registers the application for Credential Exchange Protocol export.
*/
suspend fun register(): RegisterExportResult
/**
* Unregisters the application for Credential Exchange Protocol export.
*/
suspend fun unregister(): UnregisterExportResult
}

View File

@@ -1,68 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import androidx.credentials.providerevents.transfer.CredentialTypes
import com.bitwarden.cxf.registry.CredentialExchangeRegistry
import com.bitwarden.cxf.registry.model.RegistrationRequest
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.RegisterExportResult
import com.x8bit.bitwarden.data.platform.manager.model.UnregisterExportResult
import timber.log.Timber
/**
* Default implementation of [CredentialExchangeRegistryManager].
*/
class CredentialExchangeRegistryManagerImpl(
private val credentialExchangeRegistry: CredentialExchangeRegistry,
private val settingsDiskSource: SettingsDiskSource,
) : CredentialExchangeRegistryManager {
override suspend fun register(): RegisterExportResult = credentialExchangeRegistry
.register(
registrationRequest = RegistrationRequest(
appNameRes = R.string.app_name,
credentialTypes = setOf(
CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH,
CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY,
CredentialTypes.CREDENTIAL_TYPE_ADDRESS,
CredentialTypes.CREDENTIAL_TYPE_API_KEY,
CredentialTypes.CREDENTIAL_TYPE_CREDIT_CARD,
CredentialTypes.CREDENTIAL_TYPE_CUSTOM_FIELDS,
CredentialTypes.CREDENTIAL_TYPE_DRIVERS_LICENSE,
CredentialTypes.CREDENTIAL_TYPE_IDENTITY_DOCUMENT,
CredentialTypes.CREDENTIAL_TYPE_NOTE,
CredentialTypes.CREDENTIAL_TYPE_PASSPORT,
CredentialTypes.CREDENTIAL_TYPE_PERSON_NAME,
CredentialTypes.CREDENTIAL_TYPE_SSH_KEY,
CredentialTypes.CREDENTIAL_TYPE_TOTP,
CredentialTypes.CREDENTIAL_TYPE_WIFI,
),
iconResId = BitwardenDrawable.logo_bitwarden_icon,
),
)
.fold(
onSuccess = {
Timber.d("Successfully registered for CXP export")
settingsDiskSource.storeAppRegisteredForExport(isRegistered = true)
RegisterExportResult.Success
},
onFailure = {
Timber.e(it, "Failed to register for CXP export")
RegisterExportResult.Failure(it)
},
)
override suspend fun unregister(): UnregisterExportResult = credentialExchangeRegistry
.unregister()
.fold(
onSuccess = {
Timber.d("Successfully unregistered for CXP export")
settingsDiskSource.storeAppRegisteredForExport(isRegistered = false)
UnregisterExportResult.Success
},
onFailure = {
Timber.e(it, "Failed to unregister for CXP export")
UnregisterExportResult.Failure(it)
},
)
}

View File

@@ -63,12 +63,6 @@ interface FirstTimeActionManager {
*/
fun storeShowUnlockSettingBadge(showBadge: Boolean)
/**
* Stores the given value for whether or not the active user has signalled they want to
* enable the browser autofill integration later, during onboarding.
*/
fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean)
/**
* Stores the given value for whether or not the active user has signalled they want to
* enable autofill later, during onboarding.

View File

@@ -4,7 +4,6 @@ import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
@@ -26,14 +25,12 @@ import javax.inject.Inject
/**
* Implementation of [FirstTimeActionManager]
*/
@Suppress("TooManyFunctions")
class FirstTimeActionManagerImpl @Inject constructor(
dispatcherManager: DispatcherManager,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val autofillEnabledManager: AutofillEnabledManager,
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
) : FirstTimeActionManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
@@ -81,12 +78,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
combine(
getShowAutofillSettingBadgeFlowInternal(userId = it),
getShowBrowserAutofillSettingBadgeFlowInternal(userId = it),
) { showAutofillBadge, showBrowserAutofillBadge ->
listOf(showAutofillBadge, showBrowserAutofillBadge)
}
// Can be expanded to support multiple autofill settings
getShowAutofillSettingBadgeFlowInternal(userId = it)
.map { showAutofillBadge ->
listOfNotNull(showAutofillBadge)
}
.map { list ->
list.count { showBadge -> showBadge }
}
@@ -128,7 +124,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
getShowBrowserAutofillSettingBadgeFlowInternal(userId = activeUserId),
),
) {
FirstTimeState(
@@ -136,11 +131,19 @@ class FirstTimeActionManagerImpl @Inject constructor(
showSetupUnlockCard = it[1],
showSetupAutofillCard = it[2],
showImportLoginsCardInSettings = it[3],
showSetupBrowserAutofillCard = it[4],
)
}
}
.onStart { emit(currentOrDefaultUserFirstTimeState) }
.onStart {
emit(
FirstTimeState(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
showImportLoginsCardInSettings = null,
),
)
}
.distinctUntilChanged()
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
@@ -173,11 +176,14 @@ class FirstTimeActionManagerImpl @Inject constructor(
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
showImportLoginsCardInSettings = settingsDiskSource
.getShowImportLoginsSettingBadge(it),
showSetupBrowserAutofillCard = settingsDiskSource
.getShowBrowserAutofillSettingBadge(it),
)
}
?: FirstTimeState()
?: FirstTimeState(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
showImportLoginsCardInSettings = null,
)
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
@@ -187,14 +193,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
)
}
override fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
settingsDiskSource.storeShowBrowserAutofillSettingBadge(
userId = activeUserId,
showBadge = showBadge,
)
}
override fun storeShowAutoFillSettingBadge(showBadge: Boolean) {
val activeUserId = authDiskSource.userState?.activeUserId ?: return
settingsDiskSource.storeShowAutoFillSettingBadge(
@@ -259,19 +257,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
}
}
/**
* Internal implementation to get a flow of the showBrowserAutofill value which takes
* into account if autofill and if browser autofill is already enabled.
*/
private fun getShowBrowserAutofillSettingBadgeFlowInternal(userId: String): Flow<Boolean> =
combine(
settingsDiskSource.getShowBrowserAutofillSettingBadgeFlow(userId = userId),
autofillEnabledManager.isAutofillEnabledStateFlow,
thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatusFlow,
) { showBadge, autofillEnabled, status ->
showBadge ?: false && autofillEnabled && status.isAnyIsAvailableAndDisabled
}
/**
* Internal implementation to get a flow of the showAutofill value which takes
* into account if autofill is already enabled globally.

View File

@@ -1,4 +1,4 @@
package com.bitwarden.data.manager
package com.x8bit.bitwarden.data.platform.manager
/**
* Manager for loading native libraries.

View File

@@ -1,4 +1,4 @@
package com.bitwarden.data.manager
package com.x8bit.bitwarden.data.platform.manager
import com.bitwarden.annotation.OmitFromCoverage
import timber.log.Timber
@@ -7,7 +7,7 @@ import timber.log.Timber
* Primary implementation of [NativeLibraryManager].
*/
@OmitFromCoverage
internal class NativeLibraryManagerImpl : NativeLibraryManager {
class NativeLibraryManagerImpl : NativeLibraryManager {
override fun loadLibrary(libraryName: String): Result<Unit> {
return try {
System.loadLibrary(libraryName)

View File

@@ -17,12 +17,4 @@ interface PolicyManager {
* Get all the policies of the given [type] that are enabled and applicable to the user.
*/
fun getActivePolicies(type: PolicyTypeJson): List<SyncResponseJson.Policy>
/**
* Get all the policies of the given [type] that are enabled and applicable to the [userId].
*/
fun getUserPolicies(
userId: String,
type: PolicyTypeJson,
): List<SyncResponseJson.Policy>
}

View File

@@ -54,18 +54,6 @@ class PolicyManagerImpl(
}
?: emptyList()
override fun getUserPolicies(
userId: String,
type: PolicyTypeJson,
): List<SyncResponseJson.Policy> =
this
.filterPolicies(
userId = userId,
type = type,
policies = authDiskSource.getPolicies(userId = userId),
)
.orEmpty()
/**
* A helper method to filter policies.
*/

View File

@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
*/
interface PushManager {
/**
* Flow that represents requests intended for full syncs for the user ID provided.
* Flow that represents requests intended for full syncs.
*/
val fullSyncFlow: Flow<String>
val fullSyncFlow: Flow<Unit>
/**
* Flow that represents requests intended to log a user out.
@@ -52,7 +52,7 @@ interface PushManager {
/**
* Flow that represents requests intended to trigger syncing organization keys.
*/
val syncOrgKeysFlow: Flow<String>
val syncOrgKeysFlow: Flow<Unit>
/**
* Flow that represents requests intended to trigger a sync send delete.

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.manager
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.manager.DispatcherManager
@@ -14,7 +13,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload
import com.x8bit.bitwarden.data.platform.manager.model.NotificationType
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.manager.model.PushNotificationLogOutReason
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
@@ -29,7 +27,6 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.Clock
import java.time.ZoneOffset
import java.time.ZonedDateTime
@@ -46,20 +43,18 @@ private val PUSH_TOKEN_UPDATE_DELAY: Duration = 7.days
/**
* Primary implementation of [PushManager].
*/
@Suppress("LongParameterList")
class PushManagerImpl @Inject constructor(
private val authDiskSource: AuthDiskSource,
private val pushDiskSource: PushDiskSource,
private val pushService: PushService,
private val clock: Clock,
private val json: Json,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
) : PushManager {
private val ioScope = CoroutineScope(dispatcherManager.io)
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutablePasswordlessRequestSharedFlow =
bufferedMutableSharedFlow<PasswordlessRequestData>()
@@ -71,13 +66,13 @@ class PushManagerImpl @Inject constructor(
bufferedMutableSharedFlow<SyncFolderDeleteData>()
private val mutableSyncFolderUpsertSharedFlow =
bufferedMutableSharedFlow<SyncFolderUpsertData>()
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<String>()
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableSyncSendDeleteSharedFlow =
bufferedMutableSharedFlow<SyncSendDeleteData>()
private val mutableSyncSendUpsertSharedFlow =
bufferedMutableSharedFlow<SyncSendUpsertData>()
override val fullSyncFlow: SharedFlow<String>
override val fullSyncFlow: SharedFlow<Unit>
get() = mutableFullSyncSharedFlow.asSharedFlow()
override val logoutFlow: SharedFlow<NotificationLogoutData>
@@ -98,7 +93,7 @@ class PushManagerImpl @Inject constructor(
override val syncFolderUpsertFlow: SharedFlow<SyncFolderUpsertData>
get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow()
override val syncOrgKeysFlow: SharedFlow<String>
override val syncOrgKeysFlow: SharedFlow<Unit>
get() = mutableSyncOrgKeysSharedFlow.asSharedFlow()
override val syncSendDeleteFlow: SharedFlow<SyncSendDeleteData>
@@ -134,8 +129,8 @@ class PushManagerImpl @Inject constructor(
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun onMessageReceived(notification: BitwardenNotification) {
if (authDiskSource.uniqueAppId == notification.contextId) return
val userId = activeUserId ?: return
Timber.d("Push Notification Received: ${notification.notificationType}")
when (val type = notification.notificationType) {
NotificationType.AUTH_REQUEST,
@@ -161,15 +156,8 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.UserNotification>(
string = notification.payload,
)
.takeUnless {
featureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) &&
it.pushNotificationLogOutReason ==
PushNotificationLogOutReason.KDF_CHANGE
}
?.userId
?.let {
mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(userId = it))
}
.userId
?.let { mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(it)) }
}
NotificationType.SYNC_CIPHER_CREATE,
@@ -201,24 +189,16 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncCipherNotification>(
string = notification.payload,
)
.takeIf { it.userId != null && it.cipherId != null }
?.let {
SyncCipherDeleteData(
userId = requireNotNull(it.userId),
cipherId = requireNotNull(it.cipherId),
)
}
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(it) }
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.cipherId
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(SyncCipherDeleteData(it)) }
}
NotificationType.SYNC_CIPHERS,
NotificationType.SYNC_SETTINGS,
NotificationType.SYNC_VAULT,
-> {
json
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
.userId
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
mutableFullSyncSharedFlow.tryEmit(Unit)
}
NotificationType.SYNC_FOLDER_CREATE,
@@ -246,24 +226,15 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncFolderNotification>(
string = notification.payload,
)
.takeIf { it.userId != null && it.folderId != null }
?.let {
SyncFolderDeleteData(
userId = requireNotNull(it.userId),
folderId = requireNotNull(it.folderId),
)
}
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.folderId
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
}
NotificationType.SYNC_ORG_KEYS -> {
json
.decodeFromString<NotificationPayload.SynchronizeOrganizationKeysNotifications>(
string = notification.payload,
)
.userId
.takeIf { authDiskSource.userState?.accounts.orEmpty().containsKey(it) }
?.let { mutableSyncOrgKeysSharedFlow.tryEmit(it) }
if (isLoggedIn(userId)) {
mutableSyncOrgKeysSharedFlow.tryEmit(Unit)
}
}
NotificationType.SYNC_SEND_CREATE,
@@ -291,14 +262,9 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncSendNotification>(
string = notification.payload,
)
.takeIf { it.userId != null && it.sendId != null }
?.let {
SyncSendDeleteData(
userId = requireNotNull(it.userId),
sendId = requireNotNull(it.sendId),
)
}
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.sendId
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(SyncSendDeleteData(it)) }
}
}
}

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager
import android.os.Build
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.data.manager.NativeLibraryManager
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
@@ -14,18 +13,14 @@ class SdkClientManagerImpl(
sdkRepoFactory: SdkRepositoryFactory,
private val featureFlagManager: FeatureFlagManager,
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
Client(
tokenProvider = sdkRepoFactory.getClientManagedTokens(userId = userId),
settings = null,
)
.apply {
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
userId?.let {
platform().state().apply {
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
}
Client(settings = null).apply {
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
userId?.let {
platform().state().apply {
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
}
}
}
},
) : SdkClientManager {
private val userIdToClientMap = mutableMapOf<String?, Client>()

View File

@@ -7,11 +7,8 @@ import com.bitwarden.core.data.manager.realtime.RealtimeManager
import com.bitwarden.core.data.manager.realtime.RealtimeManagerImpl
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
import com.bitwarden.cxf.registry.CredentialExchangeRegistry
import com.bitwarden.cxf.registry.dsl.credentialExchangeRegistry
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.manager.DispatcherManagerImpl
import com.bitwarden.data.manager.NativeLibraryManager
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.BitwardenServiceClient
import com.bitwarden.network.service.EventService
@@ -21,7 +18,6 @@ import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
@@ -36,8 +32,6 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
import com.x8bit.bitwarden.data.platform.manager.CertificateManagerImpl
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManager
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManagerImpl
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManagerImpl
import com.x8bit.bitwarden.data.platform.manager.DebugMenuFeatureFlagManagerImpl
@@ -47,6 +41,8 @@ import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -246,6 +242,10 @@ object PlatformManagerModule {
)
}
@Provides
@Singleton
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
@Provides
@Singleton
fun provideSdkClientManager(
@@ -302,7 +302,6 @@ object PlatformManagerModule {
dispatcherManager: DispatcherManager,
clock: Clock,
json: Json,
featureFlagManager: FeatureFlagManager,
): PushManager = PushManagerImpl(
authDiskSource = authDiskSource,
pushDiskSource = pushDiskSource,
@@ -310,7 +309,6 @@ object PlatformManagerModule {
dispatcherManager = dispatcherManager,
clock = clock,
json = json,
featureFlagManager = featureFlagManager,
)
@Provides
@@ -357,14 +355,12 @@ object PlatformManagerModule {
vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
autofillEnabledManager: AutofillEnabledManager,
thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
): FirstTimeActionManager = FirstTimeActionManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager,
autofillEnabledManager = autofillEnabledManager,
thirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
)
@Provides
@@ -395,10 +391,8 @@ object PlatformManagerModule {
@Singleton
fun provideSdkRepositoryFactory(
vaultDiskSource: VaultDiskSource,
bitwardenServiceClient: BitwardenServiceClient,
): SdkRepositoryFactory = SdkRepositoryFactoryImpl(
vaultDiskSource = vaultDiskSource,
bitwardenServiceClient = bitwardenServiceClient,
)
@Provides
@@ -428,22 +422,4 @@ object PlatformManagerModule {
clock = clock,
)
}
@Provides
@Singleton
fun provideCredentialExchangeRegistry(
application: Application,
): CredentialExchangeRegistry = credentialExchangeRegistry(
application = application,
)
@Provides
@Singleton
fun provideCredentialExchangeRegistryManager(
credentialExchangeRegistry: CredentialExchangeRegistry,
settingsDiskSource: SettingsDiskSource,
): CredentialExchangeRegistryManager = CredentialExchangeRegistryManagerImpl(
credentialExchangeRegistry = credentialExchangeRegistry,
settingsDiskSource = settingsDiskSource,
)
}

View File

@@ -8,7 +8,6 @@ data class FirstTimeState(
val showImportLoginsCardInSettings: Boolean,
val showSetupUnlockCard: Boolean,
val showSetupAutofillCard: Boolean,
val showSetupBrowserAutofillCard: Boolean,
) {
/**
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
@@ -19,12 +18,10 @@ data class FirstTimeState(
showSetupUnlockCard: Boolean? = null,
showSetupAutofillCard: Boolean? = null,
showImportLoginsCardInSettings: Boolean? = null,
showSetupBrowserAutofillCard: Boolean? = null,
) : this(
showImportLoginsCard = showImportLoginsCard ?: true,
showSetupUnlockCard = showSetupUnlockCard ?: false,
showSetupAutofillCard = showSetupAutofillCard ?: false,
showImportLoginsCardInSettings = showImportLoginsCardInSettings ?: false,
showSetupBrowserAutofillCard = showSetupBrowserAutofillCard ?: false,
)
}

View File

@@ -50,15 +50,9 @@ sealed class NotificationPayload {
*/
@Serializable
data class UserNotification(
@JsonNames("UserId", "userId")
override val userId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@Contextual
@JsonNames("Date", "date")
val date: ZonedDateTime?,
@JsonNames("PushNotificationLogOutReason", "pushNotificationLogOutReason")
val pushNotificationLogOutReason: PushNotificationLogOutReason?,
@JsonNames("Date", "date") val date: ZonedDateTime?,
) : NotificationPayload()
/**
@@ -80,21 +74,4 @@ sealed class NotificationPayload {
@JsonNames("UserId", "userId") override val userId: String?,
@JsonNames("Id", "id") val loginRequestId: String?,
) : NotificationPayload()
/**
* A notification payload for resynchronizing organization keys.
*/
@Serializable
data class SynchronizeOrganizationKeysNotifications(
@JsonNames("UserId", "userId") override val userId: String?,
@JsonNames("Id", "id") val loginRequestId: String?,
) : NotificationPayload()
/**
* A notification payload for syncing a users vault.
*/
@Serializable
data class SyncNotification(
@JsonNames("UserId", "userId") override val userId: String?,
) : NotificationPayload()
}

View File

@@ -1,11 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
import kotlinx.serialization.SerialName
/**
* Enumerated values to represent the possible reasons for a log out push notification
*/
enum class PushNotificationLogOutReason {
@SerialName("0")
KDF_CHANGE,
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Represents the result of registering for export.
*/
sealed class RegisterExportResult {
/**
* Registration was successful.
*/
data object Success : RegisterExportResult()
/**
* Registration failed.
*/
data class Failure(val throwable: Throwable?) : RegisterExportResult()
}

View File

@@ -2,8 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager.model
import android.os.Parcelable
import androidx.credentials.CredentialManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.ui.platform.manager.share.model.ShareData
import com.bitwarden.ui.platform.manager.IntentManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@@ -31,7 +30,7 @@ sealed class SpecialCircumstance : Parcelable {
*/
@Parcelize
data class ShareNewSend(
val data: ShareData,
val data: IntentManager.ShareData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
@@ -134,14 +133,6 @@ sealed class SpecialCircumstance : Parcelable {
@Parcelize
data object VerificationCodeShortcut : SpecialCircumstance()
/**
* The app was launched to select an account to export credentials from.
*/
@Parcelize
data class CredentialExchangeExport(
val data: ImportCredentialsRequestData,
) : SpecialCircumstance()
/**
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
* cleared after a successful login.

View File

@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
/**
* Required data for sync cipher delete operations.
*
* @property cipherId The cipher ID.
*/
data class SyncCipherDeleteData(
val userId: String,
val cipherId: String,
)

View File

@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
/**
* Required data for sync folder delete operations.
*
* @property folderId The folder ID.
*/
data class SyncFolderDeleteData(
val userId: String,
val folderId: String,
)

View File

@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
/**
* Required data for sync send delete operations.
*
* @property sendId The send ID.
*/
data class SyncSendDeleteData(
val userId: String,
val sendId: String,
)

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Represents the result of unregistering for Credential Exchange Protocol export.
*/
sealed class UnregisterExportResult {
/**
* Represents a successful unregistering for Credential Exchange Protocol export.
*/
data object Success : UnregisterExportResult()
/**
* Represents a failure to unregister for Credential Exchange Protocol export.
*/
data class Failure(val throwable: Throwable?) : UnregisterExportResult()
}

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