mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 19:36:34 -05:00
Compare commits
249 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d71f0c5d6 | ||
|
|
23e4b1163c | ||
|
|
317fd376a7 | ||
|
|
bc74337eae | ||
|
|
d86443c6dd | ||
|
|
d07b119802 | ||
|
|
dbf2e9f68a | ||
|
|
9ddfd376a9 | ||
|
|
dd1dbd0b97 | ||
|
|
f6be363e98 | ||
|
|
600744538d | ||
|
|
de33ba021b | ||
|
|
290f59441f | ||
|
|
94c51cacf9 | ||
|
|
6f27642a30 | ||
|
|
2ad3014da2 | ||
|
|
e6dc8e02f8 | ||
|
|
c16d31fb33 | ||
|
|
43d7b84d0a | ||
|
|
c0f8307361 | ||
|
|
064a98f86b | ||
|
|
e3b111c383 | ||
|
|
52304a266e | ||
|
|
51c23ec464 | ||
|
|
7d7951d4ca | ||
|
|
78b1676745 | ||
|
|
be27c76bd3 | ||
|
|
38bdda0a41 | ||
|
|
c61fec176a | ||
|
|
bb11b17823 | ||
|
|
562b48d689 | ||
|
|
c3496ca60f | ||
|
|
a8f8450ec9 | ||
|
|
47628a6da2 | ||
|
|
5a540a3460 | ||
|
|
92cfce1224 | ||
|
|
4597337500 | ||
|
|
e610a7541d | ||
|
|
ae4b398258 | ||
|
|
0482f9eb4d | ||
|
|
9f4bd70c8d | ||
|
|
9874aad65a | ||
|
|
97bb93c18e | ||
|
|
31e7e05eda | ||
|
|
afeeb494da | ||
|
|
d5912a5dc3 | ||
|
|
13fa8a1ed0 | ||
|
|
5a145ee163 | ||
|
|
74b9a12e19 | ||
|
|
71e830bb09 | ||
|
|
8f3f1fa3ba | ||
|
|
9bd35ccca5 | ||
|
|
74aa0a78ec | ||
|
|
ae3470c598 | ||
|
|
a70b2172cb | ||
|
|
714f7cfadc | ||
|
|
53d04375b1 | ||
|
|
3ace095b86 | ||
|
|
8a90d77fd7 | ||
|
|
4b96007a77 | ||
|
|
03df341a1e | ||
|
|
d966423087 | ||
|
|
f7cbcd21ec | ||
|
|
188ddf98f4 | ||
|
|
b8f4129691 | ||
|
|
b8482de96c | ||
|
|
9e860008e8 | ||
|
|
af737b3f07 | ||
|
|
5b5176db40 | ||
|
|
e7365b355f | ||
|
|
433b3b6fb0 | ||
|
|
318307c377 | ||
|
|
912eba14d6 | ||
|
|
837dd27106 | ||
|
|
a75e938070 | ||
|
|
054afab2cf | ||
|
|
c015c8aa43 | ||
|
|
4161020e6c | ||
|
|
5543bc6ab5 | ||
|
|
5cdee938bf | ||
|
|
62f76a4f8b | ||
|
|
7ea87505a4 | ||
|
|
c6f132d5f7 | ||
|
|
0604d15d7d | ||
|
|
5706ca2ba3 | ||
|
|
de8c344b46 | ||
|
|
957460f403 | ||
|
|
2b88743bea | ||
|
|
5243ed27d3 | ||
|
|
a7bbb81b31 | ||
|
|
2d2b740ae1 | ||
|
|
81fa635430 | ||
|
|
a1cb948257 | ||
|
|
5bb7abbf5a | ||
|
|
d98ff6478f | ||
|
|
44c373a354 | ||
|
|
3d493bb9d0 | ||
|
|
07b8115d7a | ||
|
|
9ced8647a3 | ||
|
|
b3c3365b5a | ||
|
|
266c16958d | ||
|
|
340b4f25f7 | ||
|
|
572d3357ee | ||
|
|
3a4f1d719f | ||
|
|
bebf94796c | ||
|
|
10a92dd2a3 | ||
|
|
d306813d1f | ||
|
|
97c4cd705b | ||
|
|
9fee973563 | ||
|
|
202dd65229 | ||
|
|
7849bbbb0a | ||
|
|
cd9c7f98e7 | ||
|
|
0c9530472f | ||
|
|
2636a4f93a | ||
|
|
ca474b272a | ||
|
|
acc9113f9a | ||
|
|
2eb829a25b | ||
|
|
04a1d4118f | ||
|
|
9f63cede11 | ||
|
|
a93037d63e | ||
|
|
4e57f306d3 | ||
|
|
1638a20bf0 | ||
|
|
874edfad69 | ||
|
|
0469731fba | ||
|
|
0abfa5bb97 | ||
|
|
13e6728d46 | ||
|
|
116bfd6351 | ||
|
|
6ca8a39355 | ||
|
|
24a54ce214 | ||
|
|
8d76ef50d3 | ||
|
|
22114d588a | ||
|
|
81245cf3e5 | ||
|
|
fec6479f6a | ||
|
|
a02a84ee08 | ||
|
|
df63bb4b6c | ||
|
|
2a134c619d | ||
|
|
5c5bd25d16 | ||
|
|
2363b0d619 | ||
|
|
f0946e05d5 | ||
|
|
24ccebd822 | ||
|
|
fd555e92d3 | ||
|
|
eab2c17614 | ||
|
|
617be1fd95 | ||
|
|
d5d4caea62 | ||
|
|
7bf4acbb28 | ||
|
|
2694138aa1 | ||
|
|
d2645863ea | ||
|
|
3edd5bd852 | ||
|
|
4cd5a1ed56 | ||
|
|
c122f83fa6 | ||
|
|
b558d70703 | ||
|
|
89ad7818f9 | ||
|
|
e91ba77105 | ||
|
|
cc685b2307 | ||
|
|
d14fba0c01 | ||
|
|
e965134697 | ||
|
|
df34db52e4 | ||
|
|
cf5d208516 | ||
|
|
d74040e7b9 | ||
|
|
8a2bcfade8 | ||
|
|
bc1dd730ec | ||
|
|
fa5053b5cc | ||
|
|
ad46d8d7c0 | ||
|
|
98530ed33d | ||
|
|
e57af949fc | ||
|
|
6f6aacabfb | ||
|
|
b0e0b44671 | ||
|
|
d53f3f313c | ||
|
|
4f244c52fa | ||
|
|
b4a31764c4 | ||
|
|
f4569cef2b | ||
|
|
b4926b72d9 | ||
|
|
0f899df83c | ||
|
|
ff03f49f43 | ||
|
|
2756bd9fde | ||
|
|
a39f83349f | ||
|
|
7d3ed2af88 | ||
|
|
8de465381e | ||
|
|
f22f4399be | ||
|
|
766e6b1bb9 | ||
|
|
0fb364128e | ||
|
|
0cbce39499 | ||
|
|
f954b0b941 | ||
|
|
cfd0a5b8a5 | ||
|
|
d61e1cb6f1 | ||
|
|
b31983da8b | ||
|
|
e22d309423 | ||
|
|
9b53095b5e | ||
|
|
c6814c8870 | ||
|
|
7710ad8a73 | ||
|
|
80b3a7e675 | ||
|
|
8235045dad | ||
|
|
481a8c8fbc | ||
|
|
1dc6ea2227 | ||
|
|
6554234898 | ||
|
|
e990397b29 | ||
|
|
417835ef3f | ||
|
|
39a6dd1c4b | ||
|
|
4093e61b09 | ||
|
|
c4adf3ad42 | ||
|
|
417a1494e3 | ||
|
|
ef39ea6d5d | ||
|
|
f6c20e08d1 | ||
|
|
987e065dd7 | ||
|
|
ba7ee04281 | ||
|
|
808d57edc5 | ||
|
|
3356925c7a | ||
|
|
0487d95122 | ||
|
|
0834a7a883 | ||
|
|
2b0e8f9941 | ||
|
|
0702078b04 | ||
|
|
46c7e79039 | ||
|
|
1d6e733c08 | ||
|
|
a298b85374 | ||
|
|
fe79ea4822 | ||
|
|
4c50f873e2 | ||
|
|
2bd4834b14 | ||
|
|
393931a5c6 | ||
|
|
fe6346013b | ||
|
|
41e499fdf5 | ||
|
|
aa39e6c6be | ||
|
|
eec4233486 | ||
|
|
58db64da1a | ||
|
|
a7d0d6844d | ||
|
|
249e1d3a5c | ||
|
|
d8f3e7af92 | ||
|
|
1c4e4dcaf4 | ||
|
|
9adc25471e | ||
|
|
ec6562336c | ||
|
|
f402391ed8 | ||
|
|
9b074f2106 | ||
|
|
3fa33faa35 | ||
|
|
e1434dfe21 | ||
|
|
659bbc5169 | ||
|
|
dfa1f24c30 | ||
|
|
4f65c3f7d3 | ||
|
|
0f74c3dded | ||
|
|
f7139b8b91 | ||
|
|
2b35ac0d3a | ||
|
|
4a79d7e6c8 | ||
|
|
b9a496aa57 | ||
|
|
0a398839c4 | ||
|
|
aab8198457 | ||
|
|
d2d89b5a0f | ||
|
|
ddadd0135f | ||
|
|
dc198eaf72 | ||
|
|
ff23dc3ab2 | ||
|
|
191ff4c652 | ||
|
|
99ab2245f6 |
105
.claude/CLAUDE.md
Normal file
105
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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.**
|
||||
20
.claude/prompts/review-code.md
Normal file
20
.claude/prompts/review-code.md
Normal file
@@ -0,0 +1,20 @@
|
||||
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.
|
||||
110
.claude/skills/reviewing-changes/SKILL.md
Normal file
110
.claude/skills/reviewing-changes/SKILL.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
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
11
.github/CODEOWNERS
vendored
@@ -10,6 +10,11 @@
|
||||
# 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
|
||||
@@ -48,3 +53,9 @@
|
||||
# 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
|
||||
|
||||
@@ -4,12 +4,12 @@ inputs:
|
||||
java-version:
|
||||
description: 'Java version to use'
|
||||
required: false
|
||||
default: '17'
|
||||
default: '21'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- 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@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -27,6 +27,9 @@
|
||||
],
|
||||
"matchManagers": [
|
||||
"gradle"
|
||||
],
|
||||
"excludePackageNames": [
|
||||
"com.github.bumptech.glide:compose"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
159
.github/workflows/_version.yml
vendored
Normal file
159
.github/workflows/_version.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
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
|
||||
121
.github/workflows/build-authenticator.yml
vendored
121
.github/workflows/build-authenticator.yml
vendored
@@ -15,6 +15,9 @@ 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
|
||||
@@ -28,7 +31,7 @@ on:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
JAVA_VERSION: 21
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -36,25 +39,42 @@ 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>" >> $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
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -76,13 +96,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -101,6 +121,7 @@ jobs:
|
||||
publish_playstore:
|
||||
name: Publish Authenticator Play Store artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
@@ -110,10 +131,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -145,27 +168,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
|
||||
@@ -176,7 +199,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
|
||||
@@ -186,10 +209,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@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -211,7 +234,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -219,44 +242,54 @@ 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: |
|
||||
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
VERSION_CODE="${INPUT_VERSION_CODE:-$DEFAULT_VERSION_CODE}"
|
||||
VERSION_NAME_INPUT="${INPUT_VERSION_NAME:-}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
versionCode:"$VERSION_CODE" \
|
||||
versionName:"$VERSION_NAME_INPUT"
|
||||
|
||||
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:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
|
||||
storeFile:"${{ github.workspace }}/keystores/authenticator_aab-keystore.jks" \
|
||||
storePassword:"$STORE_PASSWORD" \
|
||||
keyAlias:"authenticatorupload" \
|
||||
keyPassword:"$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:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
|
||||
storeFile:"${{ github.workspace }}/keystores/authenticator_apk-keystore.jks" \
|
||||
storePassword:"$STORE_PASSWORD" \
|
||||
keyAlias:"bitwardenauthenticator" \
|
||||
keyPassword:"$KEY_PASSWORD"
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
@@ -312,7 +345,7 @@ jobs:
|
||||
FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeAuthenticatorReleaseBundleToFirebase \
|
||||
serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }}
|
||||
serviceCredentialsFile:"$FIREBASE_CREDS_PATH"
|
||||
|
||||
# Only publish bundles to Play Store when `publish-to-play-store` is true while building
|
||||
# bundles
|
||||
@@ -322,4 +355,4 @@ jobs:
|
||||
PLAY_STORE_CREDS_FILE: ${{ github.workspace }}/secrets/authenticator_play_store-creds.json
|
||||
run: |
|
||||
bundle exec fastlane publishAuthenticatorReleaseToGooglePlayStore \
|
||||
serviceCredentialsFile:${{ env.PLAY_STORE_CREDS_FILE }} \
|
||||
serviceCredentialsFile:"$PLAY_STORE_CREDS_FILE" \
|
||||
|
||||
187
.github/workflows/build.yml
vendored
187
.github/workflows/build.yml
vendored
@@ -15,20 +15,23 @@ 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: false
|
||||
default: true
|
||||
type: boolean
|
||||
publish-to-play-store:
|
||||
description: "Optional. Deploy bundle artifact to Google Play Store"
|
||||
required: false
|
||||
default: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
JAVA_VERSION: 21
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
@@ -37,25 +40,43 @@ 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>" >> $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
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -77,13 +98,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -109,6 +130,7 @@ jobs:
|
||||
publish_playstore:
|
||||
name: Publish Play Store artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
@@ -118,10 +140,12 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -154,19 +178,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
|
||||
@@ -177,14 +201,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@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -206,7 +230,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -214,64 +238,67 @@ 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: |
|
||||
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
|
||||
VERSION_CODE="${VERSION_CODE:-$GITHUB_RUN_NUMBER}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
|
||||
versionName:${{ inputs.version-name }}
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:$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:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
|
||||
storePassword:$UPLOAD_KEYSTORE_PASSWORD \
|
||||
keyAlias:upload \
|
||||
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
keyPassword:$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:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
|
||||
storePassword:$UPLOAD_BETA_KEYSTORE_PASSWORD \
|
||||
keyAlias:bitwarden-beta-upload \
|
||||
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
keyPassword:$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:${{ env.PLAY-KEYSTORE-PASSWORD }} \
|
||||
storePassword:$PLAY_KEYSTORE_PASSWORD \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
|
||||
keyPassword:$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:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
|
||||
storePassword:$PLAY_BETA_KEYSTORE_PASSWORD \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
|
||||
keyPassword:$PLAY_BETA_KEY_PASSWORD
|
||||
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
@@ -399,8 +426,8 @@ jobs:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeReleasePlayStoreToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
actionUrl:$GITHUB_ACTION_RUN_URL \
|
||||
service_credentials_file:$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') }}
|
||||
@@ -408,8 +435,8 @@ jobs:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeBetaPlayStoreToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
actionUrl:$GITHUB_ACTION_RUN_URL \
|
||||
service_credentials_file:$APP_PLAY_FIREBASE_CREDS_PATH
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ matrix.variant == 'prod' && inputs.publish-to-play-store }}
|
||||
@@ -417,7 +444,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.ref_name == 'main') }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.event_name == 'push') }}
|
||||
run: |
|
||||
bundle exec fastlane publishProdToPlayStore
|
||||
bundle exec fastlane publishBetaToPlayStore
|
||||
@@ -425,14 +452,17 @@ jobs:
|
||||
publish_fdroid:
|
||||
name: Publish F-Droid artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -461,9 +491,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
|
||||
@@ -474,14 +504,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@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -503,7 +533,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -511,47 +541,48 @@ 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: |
|
||||
DEFAULT_VERSION_CODE=$((11000+GITHUB_RUN_NUMBER))
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
VERSION_CODE="${VERSION_CODE:-$GITHUB_RUN_NUMBER}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
versionName:$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:"${{ env.FDROID_STORE_PASSWORD }}" \
|
||||
storePassword:$FDROID_STORE_PASSWORD \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
|
||||
keyPassword:$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:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
|
||||
storePassword:$FDROID_BETA_KEYSTORE_PASSWORD \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
|
||||
keyPassword:$FDROID_BETA_KEY_PASSWORD
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -601,5 +632,5 @@ jobs:
|
||||
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeReleaseFDroidToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }}
|
||||
actionUrl:$GITHUB_ACTION_RUN_URL \
|
||||
service_credentials_file:$APP_FDROID_FIREBASE_CREDS_PATH
|
||||
|
||||
@@ -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,25 +21,26 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- 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..."
|
||||
|
||||
python .github/scripts/validate-json/validate_json.py validate $GOOGLE_FILE
|
||||
if [ $? -ne 0 ]; then
|
||||
if ! python .github/scripts/validate-json/validate_json.py validate "$GOOGLE_FILE"; then
|
||||
echo "::error::JSON validation failed for $GOOGLE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
@@ -47,14 +48,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
|
||||
@@ -65,11 +66,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
|
||||
@@ -89,10 +90,10 @@ jobs:
|
||||
fi
|
||||
|
||||
# Use echo -e to interpret escape sequences and pipe to gh pr create
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
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"
|
||||
|
||||
8
.github/workflows/crowdin-pull.yml
vendored
8
.github/workflows/crowdin-pull.yml
vendored
@@ -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,7 +16,9 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
6
.github/workflows/crowdin-push.yml
vendored
6
.github/workflows/crowdin-push.yml
vendored
@@ -16,7 +16,9 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
81
.github/workflows/github-release.yml
vendored
81
.github/workflows/github-release.yml
vendored
@@ -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,9 +25,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
@@ -40,7 +41,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)
|
||||
|
||||
@@ -52,8 +53,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")
|
||||
@@ -71,8 +72,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
|
||||
@@ -81,7 +82,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
|
||||
@@ -103,28 +104,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
|
||||
@@ -148,12 +149,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
|
||||
@@ -183,7 +184,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"
|
||||
@@ -219,12 +220,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"
|
||||
@@ -252,7 +253,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:
|
||||
@@ -263,20 +264,26 @@ 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:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$_RELEASE_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
{
|
||||
echo "# :fish_cake: Release ready at:"
|
||||
echo "$_RELEASE_URL"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [[ "$_VERSION_NAME" == "0.0.0" || "$_VERSION_NUMBER" == "0" ]]; then
|
||||
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
|
||||
{
|
||||
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"
|
||||
fi
|
||||
|
||||
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
|
||||
{
|
||||
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"
|
||||
|
||||
23
.github/workflows/publish-github-release-bwa.yml
vendored
Normal file
23
.github/workflows/publish-github-release-bwa.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
24
.github/workflows/publish-github-release-bwpm.yml
vendored
Normal file
24
.github/workflows/publish-github-release-bwpm.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
36
.github/workflows/publish-github-release.yml
vendored
36
.github/workflows/publish-github-release.yml
vendored
@@ -1,36 +0,0 @@
|
||||
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
|
||||
224
.github/workflows/publish-store.yml
vendored
224
.github/workflows/publish-store.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Publish to Google Play
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
run-name: >
|
||||
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -17,15 +18,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
|
||||
@@ -46,6 +47,10 @@ 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 }}"
|
||||
@@ -54,107 +59,132 @@ 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
|
||||
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
|
||||
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"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- 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: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
- 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: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- 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: Retrieve secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
mkdir -p ${{ github.workspace }}/app/src/standardRelease
|
||||
|
||||
- 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 play_creds.json --file ${{ github.workspace }}/secrets/play_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
|
||||
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 authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
- 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: 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
|
||||
- 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"
|
||||
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
|
||||
|
||||
27
.github/workflows/release-branch.yml
vendored
27
.github/workflows/release-branch.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: 'Release Type'
|
||||
description: "Release Type"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -22,9 +22,10 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Create RC or Test Branch
|
||||
id: rc_branch
|
||||
@@ -42,10 +43,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
|
||||
@@ -66,14 +67,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:
|
||||
@@ -81,5 +82,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
|
||||
|
||||
28
.github/workflows/respond.yml
vendored
Normal file
28
.github/workflows/respond.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
20
.github/workflows/review-code.yml
vendored
Normal file
20
.github/workflows/review-code.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches-ignore:
|
||||
- main
|
||||
pull_request_target:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- main
|
||||
|
||||
109
.github/workflows/sdlc-sdk-update.yml
vendored
109
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -22,6 +22,10 @@ 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
|
||||
@@ -54,11 +58,16 @@ 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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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
|
||||
@@ -69,18 +78,61 @@ jobs:
|
||||
id: switch-branch
|
||||
run: |
|
||||
BRANCH_NAME="sdlc/sdk-update"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git switch -c $BRANCH_NAME
|
||||
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Get current SDK version
|
||||
if git switch "$BRANCH_NAME"; then
|
||||
echo "✅ Switched to existing branch: $BRANCH_NAME"
|
||||
echo "updating_existing_branch=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "📝 Creating new branch: $BRANCH_NAME"
|
||||
git switch -c "$BRANCH_NAME"
|
||||
echo "updating_existing_branch=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Prevent updating the branch when the last committer isn't the bot
|
||||
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' "$_BRANCH_NAME")
|
||||
|
||||
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
|
||||
echo "Expected bot email: $_BOT_EMAIL"
|
||||
|
||||
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
|
||||
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
|
||||
"This indicates manual changes have been made to the branch," \
|
||||
"PR has to be merged or closed before running this workflow again."
|
||||
echo "👀 Fetching existing PR..."
|
||||
gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty'
|
||||
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ❌ Merge or close: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
|
||||
|
||||
# Using main to retrieve the changelog on consecutive updates of the same PR.
|
||||
- name: Get current SDK version from main branch
|
||||
id: get-current-sdk
|
||||
run: |
|
||||
SDK_VERSION=$(grep "bitwardenSdk =" gradle/libs.versions.toml | cut -d'"' -f2)
|
||||
git show origin/main:gradle/libs.versions.toml
|
||||
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
|
||||
if [ -z "$SDK_VERSION" ]; then
|
||||
echo "::error::Failed to get current SDK version from main branch."
|
||||
exit 1
|
||||
fi
|
||||
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
|
||||
echo "Current SDK version: $SDK_VERSION"
|
||||
echo "Current SDK version (from main): $SDK_VERSION"
|
||||
echo "Current SDK git ref: $GIT_REF"
|
||||
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
|
||||
echo "version=$SDK_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "git_ref=$GIT_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
@@ -97,14 +149,14 @@ jobs:
|
||||
run: |
|
||||
echo "👀 Committing SDK version update..."
|
||||
|
||||
git config user.name "bw-ghapp[bot]"
|
||||
git config user.email "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
git config user.name "$_BOT_NAME"
|
||||
git config user.email "$_BOT_EMAIL"
|
||||
|
||||
git add gradle/libs.versions.toml
|
||||
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
|
||||
git push origin $_BRANCH_NAME
|
||||
git push origin "$_BRANCH_NAME"
|
||||
|
||||
- name: Create Pull Request
|
||||
- name: Create or Update Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
@@ -121,17 +173,26 @@ jobs:
|
||||
|
||||
$CHANGELOG"
|
||||
|
||||
# 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")
|
||||
EXISTING_PR=$(gh pr list --head "$_BRANCH_NAME" --base main --state open --json number --jq '.[0].number // empty')
|
||||
|
||||
echo "🚀 Created PR: $PR_URL"
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "🔄 Updating existing PR #$EXISTING_PR..."
|
||||
echo -e "$PR_BODY" | gh pr edit "$EXISTING_PR" \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file -
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ✅ Updated PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "📝 Creating new PR..."
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head "$_BRANCH_NAME" \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
echo "## 🚀 Created PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Test Update
|
||||
@@ -143,7 +204,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
|
||||
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
@@ -13,10 +13,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
_JAVA_VERSION: 17
|
||||
_JAVA_VERSION: 21
|
||||
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
@@ -27,10 +26,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
@@ -52,12 +53,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env._JAVA_VERSION }}
|
||||
@@ -91,7 +92,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -103,14 +104,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
Normal file
5
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
bitwarden/gh-actions/*: ref-pin
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -11,8 +11,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-partitions (1.1177.0)
|
||||
aws-sdk-core (3.235.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.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (1.115.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-s3 (1.201.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
bigdecimal (3.3.1)
|
||||
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.13.2)
|
||||
json (2.15.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.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.20.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
|
||||
30
README.md
30
README.md
@@ -4,13 +4,12 @@
|
||||
|
||||
- [Compatibility](#compatibility)
|
||||
- [Setup](#setup)
|
||||
- [Theme](#theme)
|
||||
- [Dependencies](#dependencies)
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 35
|
||||
- **Minimum SDK**: 29 (Android 10)
|
||||
- **Target SDK**: 36 (Android 16)
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
@@ -52,12 +51,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` `17`:
|
||||
4. Setup JDK `Version` `21`:
|
||||
|
||||
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
|
||||
- Hit the selected Gradle JDK next to `Gradle JDK:`.
|
||||
- Select a `17.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `17`.
|
||||
- Select a `21.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `21`.
|
||||
- Select your preferred `Vendor`.
|
||||
- Hit `Download`.
|
||||
- Hit `Apply`.
|
||||
@@ -93,25 +92,6 @@ 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
|
||||
|
||||
@@ -224,6 +224,7 @@ dependencies {
|
||||
|
||||
implementation(project(":annotation"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":cxf"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":ui"))
|
||||
@@ -234,8 +235,6 @@ 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)
|
||||
@@ -245,6 +244,8 @@ 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)
|
||||
@@ -258,7 +259,6 @@ 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,7 +269,6 @@ 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)
|
||||
@@ -290,7 +289,7 @@ dependencies {
|
||||
testImplementation(libs.google.hilt.android.testing)
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
testImplementation(libs.junit.junit5)
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(libs.junit.vintage)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.mockk.mockk)
|
||||
@@ -303,7 +302,10 @@ tasks {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
|
||||
// Explicitly setting the user Country and Language because tests assume en-US
|
||||
"-Duser.country=US" +
|
||||
"-Duser.language=en"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,17 @@
|
||||
<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
|
||||
@@ -249,7 +260,7 @@
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill"
|
||||
android:label="@string/autofill_title"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -48,6 +48,18 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -38,6 +39,7 @@ 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
|
||||
@@ -68,6 +70,16 @@ 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
|
||||
@@ -88,7 +100,14 @@ class MainActivity : AppCompatActivity() {
|
||||
SetupEventsEffect(navController = navController)
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
|
||||
LocalManagerProvider(
|
||||
featureFlagsState = state.featureFlagsState,
|
||||
authTabLaunchers = AuthTabLaunchers(
|
||||
duo = duoLauncher,
|
||||
sso = ssoLauncher,
|
||||
webAuthn = webAuthnLauncher,
|
||||
),
|
||||
) {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{ mainViewModel.trySendAction(MainAction.ResumeScreenDataReceived(it)) }
|
||||
|
||||
@@ -2,17 +2,23 @@ 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.IntentManager
|
||||
import com.bitwarden.ui.platform.manager.share.ShareManager
|
||||
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
|
||||
@@ -75,7 +81,7 @@ class MainViewModel @Inject constructor(
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val bitwardenCredentialManager: BitwardenCredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
@@ -179,6 +185,9 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -207,6 +216,20 @@ 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()
|
||||
@@ -272,7 +295,7 @@ class MainViewModel @Inject constructor(
|
||||
val passwordlessRequestData = intent.getPasswordlessRequestDataIntentOrNull()
|
||||
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
|
||||
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||
val shareData = shareManager.getShareDataOrNull(intent = intent)
|
||||
val totpData: TotpData? =
|
||||
// First grab TOTP URI directly from the intent data:
|
||||
intent.getTotpDataOrNull()
|
||||
@@ -295,6 +318,7 @@ 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 {
|
||||
@@ -418,6 +442,16 @@ class MainViewModel @Inject constructor(
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
|
||||
importCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CredentialExchangeExport(
|
||||
data = ImportCredentialsRequestData(
|
||||
uri = importCredentialsRequest.uri,
|
||||
requestJson = importCredentialsRequest.request.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +519,21 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -216,25 +216,59 @@ 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].
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,7 @@ 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"
|
||||
@@ -67,6 +68,7 @@ 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 =
|
||||
@@ -82,6 +84,8 @@ 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?
|
||||
@@ -142,6 +146,7 @@ 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)
|
||||
@@ -329,10 +334,24 @@ 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?,
|
||||
@@ -347,10 +366,32 @@ 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))
|
||||
|
||||
@@ -579,6 +620,12 @@ class AuthDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePinProtectedUserKeyEnvelopeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyEnvelopeFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun migrateAccountTokens() {
|
||||
userState
|
||||
?.accounts
|
||||
|
||||
@@ -27,6 +27,12 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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].
|
||||
@@ -13,3 +16,36 @@ 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())
|
||||
}
|
||||
|
||||
@@ -10,19 +10,20 @@ 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 { userId ->
|
||||
authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.takeIf { it.accessToken != null }
|
||||
?.let {
|
||||
AuthTokenData(
|
||||
userId = userId,
|
||||
accessToken = requireNotNull(it.accessToken),
|
||||
expiresAtSec = it.expiresAtSec,
|
||||
)
|
||||
}
|
||||
}
|
||||
?.let(::getAuthTokenDataOrNull)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -34,10 +35,12 @@ class UserLogoutManagerImpl(
|
||||
private val toastManager: ToastManager,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val credentialExchangeRegistryManager: CredentialExchangeRegistryManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserLogoutManager {
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val mutableLogoutEventFlow: MutableSharedFlow<LogoutEvent> =
|
||||
bufferedMutableSharedFlow()
|
||||
@@ -58,8 +61,10 @@ 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)
|
||||
@@ -80,7 +85,8 @@ 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 pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
|
||||
val pinProtectedUserKeyEnvelope = authDiskSource
|
||||
.getPinProtectedUserKeyEnvelope(userId = userId)
|
||||
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
@@ -102,9 +108,9 @@ class UserLogoutManagerImpl(
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
authDiskSource.storePinProtectedUserKey(
|
||||
authDiskSource.storePinProtectedUserKeyEnvelope(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,7 +120,7 @@ class UserLogoutManagerImpl(
|
||||
generatorDiskSource.clearData(userId = userId)
|
||||
pushDiskSource.clearData(userId = userId)
|
||||
settingsDiskSource.clearData(userId = userId)
|
||||
scope.launch {
|
||||
unconfinedScope.launch {
|
||||
passwordHistoryDiskSource.clearPasswordHistories(userId = userId)
|
||||
vaultDiskSource.deleteVaultData(userId = userId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,8 @@ 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
|
||||
@@ -25,6 +27,8 @@ 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
|
||||
@@ -117,6 +121,7 @@ object AuthManagerModule {
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
credentialExchangeRegistryManager: CredentialExchangeRegistryManager,
|
||||
): UserLogoutManager =
|
||||
UserLogoutManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
@@ -128,6 +133,7 @@ object AuthManagerModule {
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
credentialExchangeRegistryManager = credentialExchangeRegistryManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -140,4 +146,18 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ fun TrustDeviceResponse.toUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val deviceOptions = decryptionOptions
|
||||
.trustedDeviceUserDecryptionOptions
|
||||
|
||||
@@ -6,6 +6,8 @@ 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
|
||||
@@ -27,7 +29,6 @@ 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
|
||||
@@ -44,17 +45,16 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
interface AuthRepository :
|
||||
AuthenticatorProvider,
|
||||
AuthRequestManager,
|
||||
KdfManager,
|
||||
UserStateManager {
|
||||
/**
|
||||
* 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,15 +110,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -140,11 +131,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
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.
|
||||
|
||||
@@ -33,6 +33,8 @@ 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
|
||||
@@ -46,15 +48,16 @@ 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
|
||||
@@ -77,13 +80,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
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
|
||||
@@ -91,50 +90,39 @@ 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
|
||||
@@ -144,7 +132,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -163,6 +151,7 @@ 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,
|
||||
@@ -172,12 +161,15 @@ class AuthRepositoryImpl(
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
private val userStateManager: UserStateManager,
|
||||
private val kdfManager: KdfManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager {
|
||||
AuthRequestManager by authRequestManager,
|
||||
KdfManager by kdfManager,
|
||||
UserStateManager by userStateManager {
|
||||
/**
|
||||
* 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
|
||||
@@ -190,24 +182,6 @@ 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.
|
||||
@@ -268,68 +242,6 @@ 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()
|
||||
|
||||
@@ -358,9 +270,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||
get() = policyManager.getActivePolicies()
|
||||
|
||||
@@ -379,7 +288,7 @@ class AuthRepositoryImpl(
|
||||
|
||||
init {
|
||||
combine(
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
userStateManager.hasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
@@ -398,11 +307,21 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
.onEach {
|
||||
val userId = activeUserId ?: return@onEach
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
vaultRepository.sync(forced = true)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
|
||||
// happens on a background thread
|
||||
@@ -460,16 +379,12 @@ 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())
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
return authSdkSource
|
||||
.hashPassword(
|
||||
email = profile.email,
|
||||
@@ -489,7 +404,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun deleteAccountWithOneTimePassword(
|
||||
oneTimePassword: String,
|
||||
): DeleteAccountResult {
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
return accountsService
|
||||
.deleteAccount(
|
||||
masterPasswordHash = null,
|
||||
@@ -501,13 +416,13 @@ class AuthRepositoryImpl(
|
||||
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
||||
fold(
|
||||
onFailure = {
|
||||
clearPendingAccountDeletion()
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
DeleteAccountResult.Error(error = it, message = null)
|
||||
},
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is DeleteAccountResponseJson.Invalid -> {
|
||||
clearPendingAccountDeletion()
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
DeleteAccountResult.Error(message = response.message, error = null)
|
||||
}
|
||||
|
||||
@@ -838,7 +753,17 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationCodeResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -850,8 +775,18 @@ class AuthRepositoryImpl(
|
||||
resendNewDeviceOtpRequestJson
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onFailure = { ResendEmailResult.Error(message = null, error = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationOtpResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -874,7 +809,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
|
||||
hasPendingAccountAddition = false
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
return SwitchAccountResult.NoChange
|
||||
}
|
||||
|
||||
@@ -889,7 +824,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||
|
||||
// Clear any pending account additions
|
||||
hasPendingAccountAddition = false
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
@@ -1088,12 +1023,6 @@ 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(
|
||||
@@ -1109,6 +1038,10 @@ 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
|
||||
},
|
||||
@@ -1360,8 +1293,8 @@ class AuthRepositoryImpl(
|
||||
?.activeAccount
|
||||
?.profile
|
||||
?: return ValidatePinResult.Error(error = NoActiveUserException())
|
||||
val pinProtectedUserKey = authDiskSource
|
||||
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||
val pinProtectedUserKeyEnvelope = authDiskSource
|
||||
.getPinProtectedUserKeyEnvelope(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error(
|
||||
error = MissingPropertyException("Pin Protected User Key"),
|
||||
)
|
||||
@@ -1369,7 +1302,7 @@ class AuthRepositoryImpl(
|
||||
.validatePin(
|
||||
userId = activeAccount.userId,
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
pinProtectedUserKey = pinProtectedUserKeyEnvelope,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ValidatePinResult.Success(isValid = it) },
|
||||
@@ -1552,27 +1485,6 @@ 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.
|
||||
*/
|
||||
@@ -1683,7 +1595,7 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel?,
|
||||
orgIdentifier: String?,
|
||||
userConfirmedKeyConnector: Boolean,
|
||||
): LoginResult = userStateTransaction {
|
||||
): LoginResult = userStateManager.userStateTransaction {
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
@@ -1722,7 +1634,7 @@ class AuthRepositoryImpl(
|
||||
// we should ask him to confirm the domain
|
||||
if (isNewKeyConnectorUser && isNotConfirmed) {
|
||||
keyConnectorResponse = loginResponse
|
||||
return LoginResult.ConfirmKeyConnectorDomain(
|
||||
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
|
||||
domain = keyConnectorUrl,
|
||||
)
|
||||
}
|
||||
@@ -1773,6 +1685,16 @@ 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.
|
||||
@@ -1962,15 +1884,27 @@ 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.Password(
|
||||
password = masterPassword,
|
||||
userKey = key,
|
||||
),
|
||||
initUserCryptoMethod = initUserCryptoMethod,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2156,22 +2090,6 @@ 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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,11 +10,15 @@ 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
|
||||
@@ -22,6 +26,7 @@ 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
|
||||
@@ -49,6 +54,7 @@ object AuthRepositoryModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
configDiskSource: ConfigDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
@@ -60,8 +66,9 @@ object AuthRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
userStateManager: UserStateManager,
|
||||
kdfManager: KdfManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
@@ -71,6 +78,7 @@ object AuthRepositoryModule {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -83,7 +91,24 @@ object AuthRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -122,6 +122,42 @@ sealed class PolicyInformation {
|
||||
val minutes: Int?,
|
||||
|
||||
@SerialName("action")
|
||||
val action: String?,
|
||||
) : PolicyInformation()
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
|
||||
*/
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
val error: Throwable?,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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()
|
||||
}
|
||||
@@ -75,6 +75,7 @@ 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.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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"
|
||||
|
||||
@@ -16,21 +19,44 @@ 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
|
||||
) {
|
||||
val code = localData.getQueryParameter("code")
|
||||
val state = localData.getQueryParameter("state")
|
||||
if (code != null && state != null) {
|
||||
DuoCallbackTokenResult.Success(token = "$code|$state")
|
||||
} else {
|
||||
DuoCallbackTokenResult.MissingToken
|
||||
}
|
||||
return if (action == Intent.ACTION_VIEW && localData != null && localData.host == DUO_HOST) {
|
||||
localData.getDuoCallbackTokenResult()
|
||||
} 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.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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
|
||||
@@ -61,21 +64,40 @@ fun generateUriForSso(
|
||||
fun Intent.getSsoCallbackResult(): SsoCallbackResult? {
|
||||
val localData = data
|
||||
return if (action == Intent.ACTION_VIEW && localData?.host == SSO_HOST) {
|
||||
val state = localData.getQueryParameter("state")
|
||||
val code = localData.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
SsoCallbackResult.Success(
|
||||
state = state,
|
||||
code = code,
|
||||
)
|
||||
} else {
|
||||
SsoCallbackResult.MissingCode
|
||||
}
|
||||
localData.getSsoCallbackResult()
|
||||
} 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.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
@@ -12,6 +14,7 @@ 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
|
||||
@@ -32,6 +35,7 @@ fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
@@ -54,6 +58,23 @@ 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,
|
||||
@@ -61,6 +82,7 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
@@ -90,6 +112,7 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
@@ -103,6 +126,30 @@ 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].
|
||||
*/
|
||||
@@ -118,6 +165,7 @@ 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,
|
||||
@@ -157,6 +205,19 @@ 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,
|
||||
@@ -185,6 +246,8 @@ 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,
|
||||
|
||||
@@ -2,6 +2,9 @@ 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
|
||||
@@ -24,15 +27,36 @@ fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
|
||||
localData != null &&
|
||||
localData.host == WEB_AUTH_HOST
|
||||
) {
|
||||
localData
|
||||
.getQueryParameter("data")
|
||||
?.let { WebAuthResult.Success(token = it) }
|
||||
?: WebAuthResult.Failure(message = localData.getQueryParameter("error"))
|
||||
localData.getWebAuthResult()
|
||||
} 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.
|
||||
*/
|
||||
@@ -59,7 +83,7 @@ fun generateUriForWebAuth(
|
||||
"?data=$base64Data" +
|
||||
"&parent=$parentParam" +
|
||||
"&v=2"
|
||||
return Uri.parse(url)
|
||||
return url.toUri()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -136,6 +136,11 @@ 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(
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -83,6 +84,7 @@ class FilledDataBuilderImpl(
|
||||
autofillCipher = autofillCipher,
|
||||
autofillViews = autofillRequest.partition.views,
|
||||
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
|
||||
packageName = autofillRequest.packageName,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -96,7 +98,9 @@ class FilledDataBuilderImpl(
|
||||
?.getOrLastOrNull(inlineSuggestionsAdded)
|
||||
|
||||
return FilledData(
|
||||
filledPartitions = filledPartitions.take(n = MAX_FILLED_PARTITIONS_COUNT),
|
||||
filledPartitions = filledPartitions
|
||||
.filter { it.filledItems.isNotEmpty() }
|
||||
.take(n = MAX_FILLED_PARTITIONS_COUNT),
|
||||
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
||||
originalPartition = autofillRequest.partition,
|
||||
uri = autofillRequest.uri,
|
||||
@@ -140,16 +144,21 @@ class FilledDataBuilderImpl(
|
||||
autofillCipher: AutofillCipher.Login,
|
||||
autofillViews: List<AutofillView.Login>,
|
||||
inlinePresentationSpec: InlinePresentationSpec?,
|
||||
packageName: String?,
|
||||
): FilledPartition {
|
||||
val filledItems = autofillViews
|
||||
.mapNotNull { autofillView ->
|
||||
val value = when (autofillView) {
|
||||
is AutofillView.Login.Username -> autofillCipher.username
|
||||
is AutofillView.Login.Password -> autofillCipher.password
|
||||
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
|
||||
}
|
||||
autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
return FilledPartition(
|
||||
|
||||
@@ -16,6 +16,8 @@ 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
|
||||
@@ -24,6 +26,8 @@ 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
|
||||
@@ -61,6 +65,22 @@ 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(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ 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
|
||||
|
||||
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -24,6 +25,7 @@ sealed class AutofillView {
|
||||
val isFocused: Boolean,
|
||||
val textValue: String?,
|
||||
val hasPasswordTerms: Boolean,
|
||||
val website: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.
|
||||
@@ -15,4 +17,20 @@ 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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,20 @@ 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.
|
||||
@@ -76,18 +90,24 @@ 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 }
|
||||
.filter { it !is AutofillView.Unused })
|
||||
.map { it.updateWebsiteIfNecessary(website = urlBarWebsite) }
|
||||
|
||||
// Find the focused view, or fallback to the first fillable item on the screen (so
|
||||
// we at least have something to hook into)
|
||||
@@ -95,16 +115,21 @@ 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 = traversalDataList.buildUriOrNull(
|
||||
val uri = focusedView.buildUriOrNull(
|
||||
packageName = packageName,
|
||||
)
|
||||
|
||||
val blockListedURIs = settingsRepository.blockedAutofillUris + BLOCK_LISTED_URIS
|
||||
if (focusedView == null || blockListedURIs.contains(uri)) {
|
||||
// The view is unfillable if there are no focused views or the URI is block listed.
|
||||
if (blockListedURIs.contains(uri)) {
|
||||
// The view is unfillable if the URI is block listed.
|
||||
return AutofillRequest.Unfillable
|
||||
}
|
||||
|
||||
@@ -165,7 +190,7 @@ private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
|
||||
.mapNotNull { windowNode ->
|
||||
windowNode
|
||||
.rootViewNode
|
||||
?.traverse()
|
||||
?.traverse(parentWebsite = null)
|
||||
?.updateForMissingPasswordFields()
|
||||
?.updateForMissingUsernameFields()
|
||||
}
|
||||
@@ -243,16 +268,25 @@ private fun ViewNodeTraversalData.copyAndMapAutofillViews(
|
||||
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
|
||||
* data into [ViewNodeTraversalData].
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
|
||||
private fun AssistStructure.ViewNode.traverse(
|
||||
parentWebsite: String?,
|
||||
): ViewNodeTraversalData {
|
||||
// Set up mutable lists for collecting valid AutofillViews and ignorable view ids.
|
||||
val mutableAutofillViewList: MutableList<AutofillView> = mutableListOf()
|
||||
val mutableIgnoreAutofillIdList: MutableList<AutofillId> = mutableListOf()
|
||||
var idPackage: String? = this.idPackage
|
||||
var website: String? = this.website
|
||||
// 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()
|
||||
|
||||
// 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()
|
||||
toAutofillView(parentWebsite = parentWebsite)
|
||||
?.run(mutableAutofillViewList::add)
|
||||
?: autofillId?.run(mutableIgnoreAutofillIdList::add)
|
||||
|
||||
@@ -260,23 +294,18 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
|
||||
for (i in 0 until childCount) {
|
||||
// Extract the traversal data from each child view node and add it to the lists.
|
||||
getChildAt(i)
|
||||
.traverse()
|
||||
.traverse(parentWebsite = website)
|
||||
.let { viewNodeTraversalData ->
|
||||
viewNodeTraversalData.autofillViews.forEach(mutableAutofillViewList::add)
|
||||
viewNodeTraversalData.ignoreAutofillIds.forEach(mutableIgnoreAutofillIdList::add)
|
||||
|
||||
// Get the first non-null 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
|
||||
if (storedIdPackage == null) {
|
||||
storedIdPackage = viewNodeTraversalData.idPackage
|
||||
}
|
||||
// Add all url bar websites. We will deal with this later if
|
||||
// there is somehow more than one.
|
||||
storedUrlBarWebsites.addAll(viewNodeTraversalData.urlBarWebsites)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,8 +313,29 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
|
||||
// descendant's.
|
||||
return ViewNodeTraversalData(
|
||||
autofillViews = mutableAutofillViewList,
|
||||
idPackage = idPackage,
|
||||
idPackage = storedIdPackage,
|
||||
urlBarWebsites = storedUrlBarWebsites,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class AutofillCipherProviderImpl(
|
||||
password = cipherView.login?.password.orEmpty(),
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
username = cipherView.login?.username.orEmpty(),
|
||||
website = uri,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ 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.
|
||||
*/
|
||||
@@ -96,3 +102,17 @@ 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) }
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
password = login.password.orEmpty(),
|
||||
subtitle = subtitle.orEmpty(),
|
||||
username = login.username.orEmpty(),
|
||||
website = uri,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,34 +49,30 @@ 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(): 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
|
||||
}
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The first supported autofill hint for this view node, or null if none are found.
|
||||
|
||||
@@ -4,36 +4,6 @@ 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].
|
||||
|
||||
@@ -24,7 +24,6 @@ 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
|
||||
@@ -93,13 +92,11 @@ object CredentialProviderModule {
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
privilegedAppRepository: PrivilegedAppRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): OriginManager =
|
||||
OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
privilegedAppRepository = privilegedAppRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
@@ -15,6 +16,7 @@ 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
|
||||
@@ -24,7 +26,6 @@ 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
|
||||
@@ -41,7 +42,6 @@ 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,8 +207,6 @@ class BitwardenCredentialManagerImpl(
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.toPublicKeyCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherListViews = cipherListViews
|
||||
.filter { it.isActiveWithFido2Credentials },
|
||||
)
|
||||
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
|
||||
|
||||
@@ -225,65 +223,72 @@ 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 = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
|
||||
.distinct()
|
||||
val relyingPartyIds = assertionOptions
|
||||
.mapNotNull { it.relyingPartyId }
|
||||
.toSet()
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException("Relying party id required.").asFailure()
|
||||
}
|
||||
|
||||
val cipherViews = cipherListViews
|
||||
.filter { cipherListView ->
|
||||
cipherListView.login
|
||||
?.fido2Credentials
|
||||
val allowedCredentials = assertionOptions
|
||||
.flatMap { option ->
|
||||
option
|
||||
.allowCredentials
|
||||
?.map { it.id }
|
||||
.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
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(
|
||||
result.error,
|
||||
"Failed to decrypt cipher while building credential entries.",
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
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()
|
||||
},
|
||||
)
|
||||
}
|
||||
.toTypedArray()
|
||||
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
|
||||
.filterAllowedCredentialsIfNecessary(allowedCredentials)
|
||||
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
return credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
cipherViews = cipherViews,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { fido2AutofillViews ->
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.asSuccess()
|
||||
},
|
||||
onFailure = {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
},
|
||||
fido2CredentialAutofillViews = discoveredCredentials,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.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(
|
||||
@@ -318,6 +323,7 @@ class BitwardenCredentialManagerImpl(
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
callingPackageName = callingAppInfo.packageName,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -342,6 +348,7 @@ class BitwardenCredentialManagerImpl(
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
callingPackageName = callingAppInfo.packageName,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -351,6 +358,7 @@ class BitwardenCredentialManagerImpl(
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
clientData: ClientData,
|
||||
callingPackageName: String,
|
||||
): Fido2RegisterCredentialResult = vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
@@ -365,7 +373,9 @@ class BitwardenCredentialManagerImpl(
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.map {
|
||||
it.toAndroidAttestationResponse(callingPackageName = callingPackageName)
|
||||
}
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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
|
||||
@@ -23,7 +21,6 @@ class OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val privilegedAppRepository: PrivilegedAppRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
@@ -70,10 +67,7 @@ class OriginManagerImpl(
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
.takeUnless {
|
||||
it is ValidateOriginResult.Error.PrivilegedAppNotAllowed &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.UserManagedPrivilegedApps)
|
||||
}
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: validatePrivilegedAppSignatureWithUserTrustList(callingAppInfo)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
|
||||
@@ -20,7 +20,7 @@ data class Fido2AttestationResponse(
|
||||
@SerialName("response")
|
||||
val response: RegistrationResponse,
|
||||
@SerialName("clientExtensionResults")
|
||||
val clientExtensionResults: ClientExtensionResults?,
|
||||
val clientExtensionResults: ClientExtensionResults,
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: String?,
|
||||
) {
|
||||
@@ -50,7 +50,7 @@ data class Fido2AttestationResponse(
|
||||
@Serializable
|
||||
data class ClientExtensionResults(
|
||||
@SerialName("credProps")
|
||||
val credentialProperties: CredentialProperties,
|
||||
val credentialProperties: CredentialProperties? = null,
|
||||
) {
|
||||
/**
|
||||
* Represents properties for newly created credential.
|
||||
|
||||
@@ -105,6 +105,16 @@ 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.
|
||||
*/
|
||||
@@ -281,6 +291,23 @@ 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.
|
||||
@@ -332,21 +359,21 @@ interface SettingsDiskSource {
|
||||
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has registered for export via the credential exchange
|
||||
* Gets whether or not the application has registered for export via the credential exchange
|
||||
* protocol.
|
||||
*/
|
||||
fun getVaultRegisteredForExport(userId: String): Boolean?
|
||||
fun getAppRegisteredForExport(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the given [userId] has registered for export via
|
||||
* Stores the given value for whether or not the application has registered for export via
|
||||
* the credential exchange protocol.
|
||||
*/
|
||||
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?)
|
||||
fun storeAppRegisteredForExport(isRegistered: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
|
||||
* Emits updates that track [getAppRegisteredForExport].
|
||||
*/
|
||||
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
|
||||
fun getAppRegisteredForExportFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the number of qualifying add cipher actions for the device.
|
||||
|
||||
@@ -37,6 +37,7 @@ 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"
|
||||
@@ -48,6 +49,8 @@ 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].
|
||||
@@ -72,6 +75,9 @@ class SettingsDiskSourceImpl(
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowBrowserAutofillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -95,8 +101,7 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableScreenCaptureAllowedFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableVaultRegisteredForExportFlow =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableVaultRegisteredForExportFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableIsDynamicColorsEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
@@ -224,6 +229,18 @@ 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)
|
||||
@@ -236,7 +253,6 @@ 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
|
||||
@@ -431,6 +447,21 @@ 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),
|
||||
@@ -483,17 +514,17 @@ class SettingsDiskSourceImpl(
|
||||
getMutableShowImportLoginsSettingBadgeFlow(userId)
|
||||
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
|
||||
|
||||
override fun getVaultRegisteredForExport(userId: String): Boolean? =
|
||||
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId))
|
||||
override fun getAppRegisteredForExport(): Boolean? =
|
||||
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT)
|
||||
|
||||
override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?) {
|
||||
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId), isRegistered)
|
||||
getMutableVaultRegisteredForExportFlow(userId).tryEmit(isRegistered)
|
||||
override fun storeAppRegisteredForExport(isRegistered: Boolean?) {
|
||||
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT, isRegistered)
|
||||
mutableVaultRegisteredForExportFlow.tryEmit(isRegistered)
|
||||
}
|
||||
|
||||
override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableVaultRegisteredForExportFlow(userId)
|
||||
.onSubscription { emit(getVaultRegisteredForExport(userId)) }
|
||||
override fun getAppRegisteredForExportFlow(userId: String): Flow<Boolean?> =
|
||||
mutableVaultRegisteredForExportFlow
|
||||
.onSubscription { emit(getAppRegisteredForExport()) }
|
||||
|
||||
override fun getAddCipherActionCount(): Int? = getInt(
|
||||
key = ADD_ACTION_COUNT,
|
||||
@@ -598,6 +629,13 @@ 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) {
|
||||
@@ -616,12 +654,6 @@ 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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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!")
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -63,6 +63,12 @@ interface FirstTimeActionManager {
|
||||
*/
|
||||
fun storeShowUnlockSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable the browser autofill integration later, during onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable autofill later, during onboarding.
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -25,12 +26,14 @@ 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)
|
||||
@@ -78,11 +81,12 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// Can be expanded to support multiple autofill settings
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOfNotNull(showAutofillBadge)
|
||||
}
|
||||
combine(
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = it),
|
||||
) { showAutofillBadge, showBrowserAutofillBadge ->
|
||||
listOf(showAutofillBadge, showBrowserAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
@@ -124,6 +128,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
),
|
||||
) {
|
||||
FirstTimeState(
|
||||
@@ -131,19 +136,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupUnlockCard = it[1],
|
||||
showSetupAutofillCard = it[2],
|
||||
showImportLoginsCardInSettings = it[3],
|
||||
showSetupBrowserAutofillCard = it[4],
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart {
|
||||
emit(
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
.onStart { emit(currentOrDefaultUserFirstTimeState) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
@@ -176,14 +173,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
|
||||
showImportLoginsCardInSettings = settingsDiskSource
|
||||
.getShowImportLoginsSettingBadge(it),
|
||||
showSetupBrowserAutofillCard = settingsDiskSource
|
||||
.getShowBrowserAutofillSettingBadge(it),
|
||||
)
|
||||
}
|
||||
?: FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
)
|
||||
?: FirstTimeState()
|
||||
|
||||
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
@@ -193,6 +187,14 @@ 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(
|
||||
@@ -257,6 +259,19 @@ 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.
|
||||
|
||||
@@ -17,4 +17,12 @@ 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>
|
||||
}
|
||||
|
||||
@@ -54,6 +54,18 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended for full syncs.
|
||||
* Flow that represents requests intended for full syncs for the user ID provided.
|
||||
*/
|
||||
val fullSyncFlow: Flow<Unit>
|
||||
val fullSyncFlow: Flow<String>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
val syncOrgKeysFlow: Flow<String>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync send delete.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -13,6 +14,7 @@ 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
|
||||
@@ -27,6 +29,7 @@ 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
|
||||
@@ -43,18 +46,20 @@ 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<Unit>()
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
@@ -66,13 +71,13 @@ class PushManagerImpl @Inject constructor(
|
||||
bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableSyncSendDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendDeleteData>()
|
||||
private val mutableSyncSendUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
|
||||
override val fullSyncFlow: SharedFlow<Unit>
|
||||
override val fullSyncFlow: SharedFlow<String>
|
||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||
|
||||
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
||||
@@ -93,7 +98,7 @@ class PushManagerImpl @Inject constructor(
|
||||
override val syncFolderUpsertFlow: SharedFlow<SyncFolderUpsertData>
|
||||
get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncOrgKeysFlow: SharedFlow<Unit>
|
||||
override val syncOrgKeysFlow: SharedFlow<String>
|
||||
get() = mutableSyncOrgKeysSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncSendDeleteFlow: SharedFlow<SyncSendDeleteData>
|
||||
@@ -129,8 +134,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,
|
||||
@@ -156,8 +161,15 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.UserNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.userId
|
||||
?.let { mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(it)) }
|
||||
.takeUnless {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.NoLogoutOnKdfChange) &&
|
||||
it.pushNotificationLogOutReason ==
|
||||
PushNotificationLogOutReason.KDF_CHANGE
|
||||
}
|
||||
?.userId
|
||||
?.let {
|
||||
mutableLogoutSharedFlow.tryEmit(NotificationLogoutData(userId = it))
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHER_CREATE,
|
||||
@@ -189,16 +201,24 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.cipherId
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(SyncCipherDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.cipherId != null }
|
||||
?.let {
|
||||
SyncCipherDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHERS,
|
||||
NotificationType.SYNC_SETTINGS,
|
||||
NotificationType.SYNC_VAULT,
|
||||
-> {
|
||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
|
||||
.userId
|
||||
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_FOLDER_CREATE,
|
||||
@@ -226,15 +246,24 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.folderId
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.folderId != null }
|
||||
?.let {
|
||||
SyncFolderDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
folderId = requireNotNull(it.folderId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_ORG_KEYS -> {
|
||||
if (isLoggedIn(userId)) {
|
||||
mutableSyncOrgKeysSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SynchronizeOrganizationKeysNotifications>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.userId
|
||||
.takeIf { authDiskSource.userState?.accounts.orEmpty().containsKey(it) }
|
||||
?.let { mutableSyncOrgKeysSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_SEND_CREATE,
|
||||
@@ -262,9 +291,14 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.sendId
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(SyncSendDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.sendId != null }
|
||||
?.let {
|
||||
SyncSendDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
sendId = requireNotNull(it.sendId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
|
||||
@@ -13,14 +14,18 @@ class SdkClientManagerImpl(
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
|
||||
Client(
|
||||
tokenProvider = sdkRepoFactory.getClientManagedTokens(userId = userId),
|
||||
settings = null,
|
||||
)
|
||||
.apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) : SdkClientManager {
|
||||
private val userIdToClientMap = mutableMapOf<String?, Client>()
|
||||
|
||||
@@ -7,8 +7,11 @@ 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
|
||||
@@ -18,6 +21,7 @@ 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
|
||||
@@ -32,6 +36,8 @@ 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
|
||||
@@ -41,8 +47,6 @@ 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
|
||||
@@ -242,10 +246,6 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
@@ -302,6 +302,7 @@ object PlatformManagerModule {
|
||||
dispatcherManager: DispatcherManager,
|
||||
clock: Clock,
|
||||
json: Json,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): PushManager = PushManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
pushDiskSource = pushDiskSource,
|
||||
@@ -309,6 +310,7 @@ object PlatformManagerModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
clock = clock,
|
||||
json = json,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -355,12 +357,14 @@ 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
|
||||
@@ -391,8 +395,10 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun provideSdkRepositoryFactory(
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): SdkRepositoryFactory = SdkRepositoryFactoryImpl(
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
bitwardenServiceClient = bitwardenServiceClient,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -422,4 +428,22 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -18,10 +19,12 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,9 +50,15 @@ 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("Date", "date")
|
||||
val date: ZonedDateTime?,
|
||||
|
||||
@JsonNames("PushNotificationLogOutReason", "pushNotificationLogOutReason")
|
||||
val pushNotificationLogOutReason: PushNotificationLogOutReason?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -74,4 +80,21 @@ 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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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()
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CredentialManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.ui.platform.manager.share.model.ShareData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
@@ -30,7 +31,7 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data class ShareNewSend(
|
||||
val data: IntentManager.ShareData,
|
||||
val data: ShareData,
|
||||
val shouldFinishWhenComplete: Boolean,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
@@ -133,6 +134,14 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object VerificationCodeShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched to select an account to export credentials from.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CredentialExchangeExport(
|
||||
val data: ImportCredentialsRequestData,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
|
||||
@@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync cipher delete operations.
|
||||
*
|
||||
* @property cipherId The cipher ID.
|
||||
*/
|
||||
data class SyncCipherDeleteData(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync folder delete operations.
|
||||
*
|
||||
* @property folderId The folder ID.
|
||||
*/
|
||||
data class SyncFolderDeleteData(
|
||||
val userId: String,
|
||||
val folderId: String,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,8 @@ 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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
|
||||
/**
|
||||
@@ -10,4 +11,9 @@ interface SdkRepositoryFactory {
|
||||
* Retrieves or creates a [CipherRepository] for use with the Bitwarden SDK.
|
||||
*/
|
||||
fun getCipherRepository(userId: String): CipherRepository
|
||||
|
||||
/**
|
||||
* Retrieves or creates a [ClientManagedTokens] for use with the Bitwarden SDK.
|
||||
*/
|
||||
fun getClientManagedTokens(userId: String?): ClientManagedTokens
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkTokenRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
|
||||
/**
|
||||
@@ -9,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
*/
|
||||
class SdkRepositoryFactoryImpl(
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val bitwardenServiceClient: BitwardenServiceClient,
|
||||
) : SdkRepositoryFactory {
|
||||
override fun getCipherRepository(
|
||||
userId: String,
|
||||
@@ -17,4 +21,12 @@ class SdkRepositoryFactoryImpl(
|
||||
userId = userId,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
)
|
||||
|
||||
override fun getClientManagedTokens(
|
||||
userId: String?,
|
||||
): ClientManagedTokens =
|
||||
SdkTokenRepository(
|
||||
userId = userId,
|
||||
tokenProvider = bitwardenServiceClient.tokenProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk.repository
|
||||
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.network.provider.TokenProvider
|
||||
|
||||
/**
|
||||
* A user-scoped implementation of a Bitwarden SDK [ClientManagedTokens].
|
||||
*/
|
||||
class SdkTokenRepository(
|
||||
private val userId: String?,
|
||||
private val tokenProvider: TokenProvider,
|
||||
) : ClientManagedTokens {
|
||||
override suspend fun getAccessToken(): String? =
|
||||
userId?.let { tokenProvider.getAccessToken(userId = it) }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user