Compare commits

...

62 Commits

Author SHA1 Message Date
David Perez
693d9f18db PM-19498: Update cipher migration logic (#4921) 2025-03-26 12:29:34 -05:00
David Perez
94a91702cc PM-19399: Do not show 'Share error details' button when user enter incorrect password (#4905) 2025-03-21 16:29:38 +00:00
David Perez
21af60f4de Update to Junit 5.12.1 (#4903) 2025-03-21 16:11:12 +00:00
Matt Andreko
1add57d56c Fix SARIF upload branch ref/sha (#4899) 2025-03-21 13:13:00 +00:00
bw-ghapp[bot]
b0421c774b Autosync Crowdin Translations (#4902)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-21 13:06:20 +00:00
David Perez
7d846ae383 PM-18862: Update IME handling for BitwardenScaffold and VaultUnlockedNavBarScreen (#4901) 2025-03-20 20:44:46 +00:00
David Perez
29371bdcb5 PM-19389: Handle encoding error when migrating biometric key (#4900) 2025-03-20 19:15:44 +00:00
Álison Fernandes
3eed1c1abe [PM-19049] Add workflow to regularly fetch updates to fido2_privileged_google.json (#4858)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-03-19 21:13:40 +00:00
aj-rosado
ad6bc883b8 [PM-13257] Checking if user is navigating from vault before showing prompt for biometrics (#4846) 2025-03-19 10:23:50 +00:00
David Perez
f4f669683e PM-19334: Propagate errors to the UI (#4893) 2025-03-18 17:27:41 +00:00
Dave Severns
475a82e0fb PM-19335 add throwable val to generator error results (#4894) 2025-03-18 17:08:11 +00:00
Phil Cappelli
f22156389b BWA-119 - Unable to Access Manual Code Entry After Denying Camera Permissions on (#4891) 2025-03-18 15:50:51 +00:00
Phil Cappelli
4f09f5dae4 PM-18872 - When a Folder name is long, the View/Edit Item > Folder selection screen doesn't adjust well (#4892) 2025-03-18 15:50:39 +00:00
David Perez
3934bc9ae2 PM-19314: Propagate remaining auth errors to the UI (#4888) 2025-03-18 15:24:17 +00:00
David Perez
72c9149d27 PM-19295: Propagate password errors to the UI (#4884) 2025-03-18 14:05:36 +00:00
David Perez
a040a38ce8 PM-19296: Propagate login errors to the UI (#4885) 2025-03-18 14:05:07 +00:00
Dave Severns
ef3b7730d0 PM-19289 propagating remaining vault result errors. (#4881) 2025-03-18 13:51:30 +00:00
David Perez
ad8d8d271a PM-19294: Propagate the Register errors to the UI (#4883) 2025-03-17 19:54:43 +00:00
David Perez
4954e57007 Update gem dependencies (#4882) 2025-03-17 19:29:22 +00:00
Dave Severns
6f50fffd17 PM-19275 propagate the errors for the vault unlock error result types (#4878) 2025-03-17 19:24:13 +00:00
David Perez
44c5755301 PM-19284: Propagate SSO flow errors to the UI (#4880) 2025-03-17 19:14:17 +00:00
David Perez
b20eece3aa PM-19283: Propagate error from email token and known device flows (#4879) 2025-03-17 17:47:07 +00:00
renovate[bot]
869a3b00a5 [deps]: Lock file maintenance (#4875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 15:47:03 +00:00
David Perez
7f8e848c46 PM-19245: Propagate error from password validation to UI (#4877) 2025-03-17 15:36:01 +00:00
Dave Severns
6c784d28eb PM-19272 propagate errors from cipher results (#4876) 2025-03-17 15:18:05 +00:00
Dave Severns
dfcdc72499 PM-19241 folder result errors propagated to UI (#4870) 2025-03-17 12:20:44 +00:00
Dave Severns
db287ddce5 PM-19243 send result errors propagated to UI (#4872) 2025-03-14 22:01:42 +00:00
David Perez
9ea85917b1 PM-19239: Propagate delete account errors to the UI (#4871) 2025-03-14 21:27:56 +00:00
Dave Severns
6db4165c4c PM-19234 propagates attachment result errors to UI (#4869) 2025-03-14 20:47:25 +00:00
David Perez
18ce45e7e5 PM-19233: Propagate auth request errors to the UI (#4868) 2025-03-14 18:33:39 +00:00
Dave Severns
6fe9eba620 PM-11356 Adjust autofocus delay to be greater than screen refresh delay. (#4866) 2025-03-14 16:48:34 +00:00
David Perez
b084987758 PM-19226: Propagate error from create auth request flow to UI (#4867) 2025-03-14 15:47:03 +00:00
bw-ghapp[bot]
5d0593026f Autosync Crowdin Translations (#4865)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-14 14:39:07 +00:00
David Perez
47abeb7843 PM-14435: Improve accessibility service detection (#4864) 2025-03-14 14:16:25 +00:00
Phil Cappelli
e2779e4edb PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4863) 2025-03-13 20:46:14 +00:00
Álison Fernandes
90cc9f77c5 [PM-19207] Add Passkey / FIDO2 Bug Report template (#4859) 2025-03-13 20:31:30 +00:00
David Perez
40760f270a Use immutable map in debug menu (#4861) 2025-03-13 19:28:27 +00:00
David Perez
8a773141a4 Simplify RootNavScreenTest (#4860) 2025-03-13 19:20:14 +00:00
David Perez
0c149abdd9 PM-18844: Update BitwardenBasicDialog to allow it to share error logs (#4855) 2025-03-13 18:16:22 +00:00
David Perez
f540f86b19 PM-19199: hoist debug menu up to top level of the app (#4857) 2025-03-13 17:51:22 +00:00
Dave Severns
ca64ce2176 PM-18877 Respect system app specific language selection on Android 13 and up. (#4849) 2025-03-13 13:14:41 +00:00
André Bispo
da63c9e36b [PM-17995] Adjust custom fields section (#4835) 2025-03-13 11:33:39 +00:00
André Bispo
e16ad44d5e [PM-17242] While on autofill search on all item types. (#4824) 2025-03-13 11:33:12 +00:00
Phil Cappelli
d26a2ee52a PM-18681 - Update Showing Coach Mark Tour Logic To Account for Org Only Policy (#4854) 2025-03-12 20:20:44 +00:00
Dave Severns
1eb741ab58 PM-11356 prevent extra soft-keyboard showing. (#4845) 2025-03-11 20:10:46 +00:00
David Perez
e10ca9a6ec PM-19099: Centralize app metadata (#4847) 2025-03-11 19:47:07 +00:00
David Perez
3fca61ad3e Remove the language change dialog (#4658) 2025-03-11 14:33:22 +00:00
David Perez
409529b9ca Update AndroidX Activity to 1.10.1 (#4844) 2025-03-11 13:51:39 +00:00
David Perez
4568dd53d4 Update Firebase BOM to 33.10.0 (#4843) 2025-03-10 21:20:56 +00:00
David Perez
b9b90165bf PM-10725: Always show share sheet after creating send regardless of how it was made (#4841) 2025-03-10 20:43:49 +00:00
David Perez
778a630012 Update to AGP 8.9.0 (#4840) 2025-03-10 15:46:30 +00:00
Dave Severns
4809066ad7 PM-17087 update notification payloads to support camelCase JSON keys. (#4823) 2025-03-10 14:54:58 +00:00
Patrick Honkonen
d03c6c243d [PM-18873] Refactor ItemHeader.kt to improve location display (#4814) 2025-03-07 17:02:15 +00:00
bw-ghapp[bot]
d19ab498ff Autosync Crowdin Translations (#4832)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-07 16:48:31 +00:00
Phil Cappelli
efc3d21fde PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4821) 2025-03-05 16:01:41 +00:00
Phil Cappelli
35e585a60e PM-18570 Update Owner Selection Field to Bottom Sheet Selector (#4810) 2025-03-04 22:24:31 +00:00
Patrick Honkonen
39787f9bf0 [deps] Update mockk to 1.13.17 (#4818) 2025-03-04 20:17:42 +00:00
Patrick Honkonen
3940997ef9 [deps] Update junit5 to 5.11.4 (#4819) 2025-03-04 20:17:23 +00:00
Patrick Honkonen
ce482e744d [deps] Update testng to 7.11.0 (#4820) 2025-03-04 19:36:18 +00:00
Patrick Honkonen
b0157d10e2 [deps] Update detekt to 1.23.8 (#4817) 2025-03-04 19:36:00 +00:00
renovate[bot]
cf3c2fb56d [deps]: migrate renovate config (#4815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 17:56:09 +00:00
Dave Severns
a88a173e00 PM-18773 update the keyName for the ChromeAutofill flag (#4812) 2025-03-03 15:31:02 +00:00
343 changed files with 4449 additions and 2100 deletions

64
.github/ISSUE_TEMPLATE/bug-passkey.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Passkey Bug Report
description: File a Passkey / FIDO2 related bug report
labels: [ "app:password-manager", "bug-passkey" ]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this Passkey-related bug report!
Please provide as much detail as possible to help us investigate the issue.
- type: dropdown
id: origin
attributes:
label: Origin
description: Are you using a web browser or a native application?
options:
- Web (Browser)
- Native Application (non-browser app)
validations:
required: true
- type: input
id: rp-id
attributes:
label: Web URL or App name
description: The website domain or app name you were trying to use the Passkey with
placeholder: "e.g. example.com or ExampleApp"
validations:
required: true
- type: checkboxes
id: operation-type
attributes:
label: Passkey Action
description: What passkey related action(s) were you trying to perform?
options:
- label: Creating new passkey (Registration)
- label: Signing in (Authentication)
validations:
required: true
- type: textarea
id: build-info
attributes:
label: Build Information
description: Please retrieve the build information from the About screen by tapping the Version number field
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Any additional context, steps to reproduce, error messages, or relevant information about the issue
- type: checkboxes
id: issue-tracking-info
attributes:
label: Issue Tracking Info
description: |
Issue tracking information
options:
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.

51
.github/renovate.json vendored
View File

@@ -1,33 +1,56 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": ["github-actions", "gradle", "bundler"],
"extends": [
"github>bitwarden/renovate-config"
],
"enabledManagers": [
"github-actions",
"gradle",
"bundler"
],
"packageRules": [
{
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
"matchManagers": [
"github-actions"
],
"matchUpdateTypes": [
"minor",
"patch"
]
},
{
"groupName": "gradle minor",
"matchUpdateTypes": ["minor", "patch"],
"matchManagers": ["gradle"]
"matchUpdateTypes": [
"minor",
"patch"
],
"matchManagers": [
"gradle"
]
},
{
"groupName": "kotlin",
"description": "Kotlin and Compose dependencies that must be updated together to maintain compatibility.",
"matchPackagePatterns": [
"androidx.compose:compose-bom",
"androidx.lifecycle:*",
"org.jetbrains.kotlin.*",
"com.google.devtools.ksp"
"matchManagers": [
"gradle"
],
"matchManagers": ["gradle"]
"matchPackageNames": [
"/androidx.compose:compose-bom/",
"/androidx.lifecycle:*/",
"/org.jetbrains.kotlin.*/",
"/com.google.devtools.ksp/"
]
},
{
"groupName": "bundler minor",
"matchUpdateTypes": ["minor", "patch"],
"matchManagers": ["bundler"]
"matchUpdateTypes": [
"minor",
"patch"
],
"matchManagers": [
"bundler"
]
}
]
}

View File

@@ -0,0 +1 @@
3.13

39
.github/scripts/validate-json/README.md vendored Normal file
View File

@@ -0,0 +1,39 @@
# JSON Validation Scripts
Utility scripts for validating JSON files and checking for duplicate package names between Google and Community privileged browser lists.
## Usage
### Validate a JSON file
```bash
python validate_json.py validate <json_file>
```
### Check for duplicates between two JSON files
```bash
python validate_json.py duplicates <json_file1> <json_file2> [output_file]
```
If `output_file` is not specified, duplicates will be saved to `duplicates.txt`.
## Running Tests
```bash
# Run all tests
python -m unittest test_validate_json.py
# Run the invalid JSON test individually
python -m unittest test_validate_json.TestValidateJson.test_validate_json_invalid
```
## Examples
```bash
# Validate Google privileged browsers list
python validate_json.py validate ../../app/src/main/assets/fido2_privileged_google.json
# Check for duplicates between Google and Community lists
python validate_json.py duplicates ../../app/src/main/assets/fido2_privileged_google.json ../../app/src/main/assets/fido2_privileged_community.json duplicates.txt
```

View File

@@ -0,0 +1,20 @@
{
"apps": [
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
}
]
}

View File

@@ -0,0 +1,48 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
}
]
}
}
]
}

View File

@@ -0,0 +1,20 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
}
]
}

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
import unittest
import os
import json
from validate_json import validate_json, find_duplicates, get_package_names
from unittest.mock import patch
import io
class TestValidateJson(unittest.TestCase):
def setUp(self):
self.valid_file = os.path.join(os.path.dirname(__file__), "fixtures/sample-valid1.json")
self.valid_file2 = os.path.join(os.path.dirname(__file__), "fixtures/sample-valid2.json")
self.invalid_file = os.path.join(os.path.dirname(__file__), "fixtures/sample-invalid.json")
# Suppress stdout
self.stdout_patcher = patch('sys.stdout', new=io.StringIO())
self.stdout_patcher.start()
def tearDown(self):
self.stdout_patcher.stop()
def test_validate_json_valid(self):
"""Test validation of valid JSON file"""
self.assertTrue(validate_json(self.valid_file))
def test_validate_json_invalid(self):
"""Test validation of invalid JSON file"""
self.assertFalse(validate_json(self.invalid_file))
def test_find_duplicates(self):
"""Test when using the same file (should find duplicates)"""
expected_package_names = get_package_names(self.valid_file)
duplicates = find_duplicates(self.valid_file, self.valid_file)
self.assertEqual(len(duplicates), len(expected_package_names))
for package_name in expected_package_names:
self.assertIn(package_name, duplicates)
def test_find_duplicates_returns_empty_list_when_no_duplicates(self):
"""Test when using different files (should not find duplicates)"""
duplicates = find_duplicates(self.valid_file, self.valid_file2)
self.assertEqual(len(duplicates), 0)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
import json
import sys
import os
from typing import List, Dict, Any, Set
def get_package_names(file_path: str) -> Set[str]:
"""
Extracts package names from a JSON file.
Args:
file_path: Path to the JSON file
Returns:
Set of package names
"""
with open(file_path, 'r') as f:
data = json.load(f)
package_names = set()
for app in data["apps"]:
package_names.add(app["info"]["package_name"])
return package_names
def validate_json(file_path: str) -> bool:
"""
Validates if a JSON file is correctly formatted by attempting to deserialize it.
Args:
file_path: Path to the JSON file to validate
Returns:
True if valid, False otherwise
"""
try:
if not os.path.exists(file_path):
print(f"Error: File {file_path} does not exist")
return False
with open(file_path, 'r') as f:
json.load(f)
print(f"✅ JSON file {file_path} is valid")
return True
except json.JSONDecodeError as e:
print(f"❌ Invalid JSON in {file_path}: {str(e)}")
return False
except Exception as e:
print(f"❌ Error validating {file_path}: {str(e)}")
return False
def find_duplicates(file1_path: str, file2_path: str) -> List[str]:
"""
Checks for duplicate package_name entries between two JSON files.
Args:
file1_path: Path to the first JSON file
file2_path: Path to the second JSON file
Returns:
List of duplicate package names, empty list if none found
"""
try:
# Get package names from both files
packages1 = get_package_names(file1_path)
packages2 = get_package_names(file2_path)
# Find duplicates
duplicates = list(packages1.intersection(packages2))
if duplicates:
print(f"❌ Found {len(duplicates)} duplicate package names between {file1_path} and {file2_path}:")
for dup in duplicates:
print(f" - {dup}")
return duplicates
else:
print(f"✅ No duplicate package names found between {file1_path} and {file2_path}")
return []
except Exception as e:
print(f"❌ Error checking duplicates: {str(e)}")
return []
def save_duplicates_to_file(duplicates: List[str], output_file: str) -> None:
"""
Saves the list of duplicates to a file.
Args:
duplicates: List of duplicate package names
output_file: Path to save the list of duplicates
"""
try:
with open(output_file, 'w') as f:
for dup in duplicates:
f.write(f"{dup}\n")
print(f"Duplicates saved to {output_file}")
except Exception as e:
print(f"❌ Error saving duplicates to file: {str(e)}")
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" Validate JSON: python validate_json.py validate <json_file>")
print(" Check duplicates: python validate_json.py duplicates <json_file1> <json_file2> [output_file]")
sys.exit(1)
command = sys.argv[1]
match command:
case "validate":
if len(sys.argv) < 3:
print("Error: Missing JSON file path")
sys.exit(1)
file_path = sys.argv[2]
success = validate_json(file_path)
sys.exit(0 if success else 1)
case "duplicates":
if len(sys.argv) < 4:
print("Error: Missing JSON file paths")
sys.exit(1)
file1_path = sys.argv[2]
file2_path = sys.argv[3]
output_file = sys.argv[4] if len(sys.argv) > 4 else "duplicates.txt"
duplicates = find_duplicates(file1_path, file2_path)
if duplicates:
save_duplicates_to_file(duplicates, output_file)
sys.exit(0)
case _:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
name: Cron / Sync Google Privileged Browsers List
on:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
env:
SOURCE_URL: https://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
GOOGLE_FILE: app/src/main/assets/fido2_privileged_google.json
COMMUNITY_FILE: app/src/main/assets/fido2_privileged_community.json
jobs:
sync-privileged-browsers:
name: Sync Google Privileged Browsers List
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: Download Google Privileged Browsers List
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
- name: Check for changes
id: check-changes
run: |
if git diff --quiet -- $GOOGLE_FILE; then
echo "👀 No changes detected, skipping..."
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
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
echo "::error::JSON validation failed for $GOOGLE_FILE"
exit 1
fi
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
if [ -f duplicates.txt ]; then
echo "::warning::Duplicate package names found between Google and Community files."
echo "duplicates_found=true" >> $GITHUB_OUTPUT
else
echo "✅ No duplicate package names found between Google and Community files"
echo "duplicates_found=false" >> $GITHUB_OUTPUT
fi
- name: Create branch and commit
if: steps.check-changes.outputs.has_changes == 'true'
run: |
echo "👀 Committing fido2_privileged_google.json..."
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 commit -m "Update Google privileged browsers list"
git push origin $BRANCH_NAME
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "🌱 Branch created: $BRANCH_NAME"
- name: Create Pull Request
if: steps.check-changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DUPLICATES_FOUND: ${{ steps.check-changes.outputs.duplicates_found }}
BASE_PR_URL: ${{ github.server_url }}/${{ github.repository }}/pull/
run: |
PR_BODY="Updates the Google privileged browsers list with the latest data from $SOURCE_URL"
if [ "$DUPLICATES_FOUND" = "true" ]; then
PR_BODY="$PR_BODY\n\n> [!WARNING]\n> :suspect: The following package(s) appear in both Google and Community files:"
while IFS= read -r line; do
PR_BODY="$PR_BODY\n> - $line"
done < duplicates.txt
fi
# Use echo -e to interpret escape sequences and pipe to gh pr create
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update Google privileged browsers list" \
--body-file - \
--base main \
--head $BRANCH_NAME \
--label "automated-pr" \
--label "t:ci")

View File

@@ -49,6 +49,8 @@ jobs:
uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2
with:
sarif_file: cx_result.sarif
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
quality:
name: Quality scan

View File

@@ -37,6 +37,8 @@ jobs:
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
sarif_file: cx_result.sarif
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
quality:
name: Quality scan

View File

@@ -44,6 +44,8 @@ jobs:
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
sarif_file: cx_result.sarif
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
quality:
name: Quality scan

7
.gitignore vendored
View File

@@ -28,3 +28,10 @@ user.properties
/app/src/standardBeta/google-services.json
/app/src/standardRelease/google-services.json
/authenticator/src/google-services.json
# Python
.python-version
__pycache__/
# Generated by .github/scripts/validate-json/validate-json.py
duplicates.txt

View File

@@ -9,17 +9,18 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1040.0)
aws-sdk-core (3.216.0)
aws-eventstream (1.3.2)
aws-partitions (1.1067.0)
aws-sdk-core (3.220.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.178.0)
aws-sdk-s3 (1.182.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -34,7 +35,7 @@ GEM
highline (~> 2.0.0)
date (3.4.1)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
@@ -69,7 +70,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.226.0)
fastlane (2.227.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -137,12 +138,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -160,15 +161,17 @@ GEM
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.9.1)
json (2.10.2)
jwt (2.10.1)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
nkf (0.2.0)
@@ -182,7 +185,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.0)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)

View File

@@ -68,7 +68,7 @@ android {
buildConfigField(
type = "String",
name = "CI_INFO",
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
)
}
@@ -104,7 +104,7 @@ android {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
@@ -115,7 +115,7 @@ android {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
@@ -180,7 +180,6 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
@Suppress("UnstableApiUsage")
testOptions {
// Required for Robolectric
unitTests.isIncludeAndroidResources = true
@@ -272,6 +271,8 @@ dependencies {
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.google.hilt.android.testing)
testImplementation(platform(libs.junit.bom))
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.junit.junit5)
testImplementation(libs.junit.vintage)
testImplementation(libs.kotlinx.coroutines.test)

View File

@@ -6,6 +6,11 @@ package com.x8bit.bitwarden
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
/**
* The short form legacy name for the accessibility service.
*/
const val LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME: String = ".Accessibility.AccessibilityService"
/**
* The legacy name for the autofill service.
*/

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
@@ -15,6 +16,7 @@ import androidx.compose.runtime.remember
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
@@ -24,16 +26,20 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
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.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.appLanguage
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Primary entry point for the application.
*/
@Suppress("TooManyFunctions")
@OmitFromCoverage
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -69,13 +75,9 @@ class MainActivity : AppCompatActivity() {
)
}
// Within the app the language and theme will change dynamically and will be managed by the
// Within the app the theme will change dynamically and will be managed by the
// OS, but we need to ensure we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
settingsRepository.appLanguage.localeName?.let { localeName ->
val localeList = LocaleListCompat.forLanguageTags(localeName)
AppCompatDelegate.setApplicationLocales(localeList)
}
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
@@ -111,7 +113,7 @@ class MainActivity : AppCompatActivity() {
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{
@@ -122,10 +124,19 @@ class MainActivity : AppCompatActivity() {
},
)
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
NavHost(
navController = navController,
)
startDestination = ROOT_ROUTE,
) {
// Nothing else should end up at this top level, we just want the ability
// to have the debug menu appear on top of the rest of the app without
// interacting with the state-based navigation used by the RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
}
@@ -140,6 +151,31 @@ class MainActivity : AppCompatActivity() {
)
}
override fun onResume() {
super.onResume()
// When the app resumes check for any app specific language which may have been
// set via the device settings. Similar to the theme setting in onCreate this
// ensures we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
val appSpecificLanguage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val locales: LocaleListCompat = AppCompatDelegate.getApplicationLocales()
if (locales.isEmpty) {
// App is using the system language
null
} else {
// App has specific language settings
locales.get(0)?.appLanguage
}
} else {
// For older versions, use what ever language is available from the repository.
settingsRepository.appLanguage
}
appSpecificLanguage?.let {
mainViewModel.trySendAction(MainAction.AppSpecificLanguageUpdate(it))
}
}
override fun onStop() {
super.onStop()
// In some scenarios on an emulator the Activity can leak when recreated

View File

@@ -19,10 +19,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -32,8 +34,10 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
@@ -54,6 +58,7 @@ import java.time.Clock
import javax.inject.Inject
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
private const val ANIMATION_REFRESH_DELAY = 500L
/**
* A view model that helps launch actions for the [MainActivity].
@@ -63,12 +68,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
featureFlagManager: FeatureFlagManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
@@ -79,6 +85,9 @@ class MainViewModel @Inject constructor(
initialState = MainState(
theme = settingsRepository.appTheme,
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
key = FlagKey.MobileErrorReporting,
),
),
) {
private var specialCircumstance: SpecialCircumstance?
@@ -96,6 +105,12 @@ class MainViewModel @Inject constructor(
.onEach { specialCircumstance = it }
.launchIn(viewModelScope)
featureFlagManager
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
accessibilitySelectionManager
.accessibilitySelectionFlow
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
@@ -134,8 +149,7 @@ class MainViewModel @Inject constructor(
// Switching between account states often involves some kind of animation (ex:
// account switcher) that we might want to give time to finish before triggering
// a refresh.
@Suppress("MagicNumber")
delay(500)
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.CurrentUserStateChange)
}
.launchIn(viewModelScope)
@@ -147,8 +161,7 @@ class MainViewModel @Inject constructor(
is VaultStateEvent.Locked -> {
// Similar to account switching, triggering this action too soon can
// interfere with animations or navigation logic, so we will delay slightly.
@Suppress("MagicNumber")
delay(500)
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.VaultUnlockStateChange)
}
@@ -172,6 +185,17 @@ class MainViewModel @Inject constructor(
}
override fun handleAction(action: MainAction) {
when (action) {
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
is MainAction.Internal -> handleInternalAction(action)
}
}
private fun handleInternalAction(action: MainAction.Internal) {
when (action) {
is MainAction.Internal.AccessibilitySelectionReceive -> {
handleAccessibilitySelectionReceive(action)
@@ -185,13 +209,24 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.Internal.OnMobileErrorReportingReceive -> {
handleOnMobileErrorReportingReceive(action)
}
}
}
private fun handleOnMobileErrorReportingReceive(
action: MainAction.Internal.OnMobileErrorReportingReceive,
) {
mutableStateFlow.update {
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
}
}
private fun handleAppSpecificLanguageUpdate(action: MainAction.AppSpecificLanguageUpdate) {
settingsRepository.appLanguage = action.appLanguage
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
@@ -445,7 +480,16 @@ class MainViewModel @Inject constructor(
data class MainState(
val theme: AppTheme,
val isScreenCaptureAllowed: Boolean,
) : Parcelable
private val isErrorReportingDialogEnabled: Boolean,
) : Parcelable {
/**
* Contains all feature flags that are available to the UI.
*/
val featureFlagsState: FeatureFlagsState
get() = FeatureFlagsState(
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
)
}
/**
* Models actions for the [MainActivity].
@@ -471,6 +515,12 @@ sealed class MainAction {
*/
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
/**
* Receive if there is an app specific locale selection made by user
* in the device's settings.
*/
data class AppSpecificLanguageUpdate(val appLanguage: AppLanguage) : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
@@ -483,6 +533,13 @@ sealed class MainAction {
val cipherView: CipherView,
) : Internal()
/**
* Indicates the Mobile Error Reporting feature flag has been updated.
*/
data class OnMobileErrorReportingReceive(
val isErrorReportingEnabled: Boolean,
) : Internal()
/**
* Indicates the user has manually selected the given [cipherView] for autofill.
*/

View File

@@ -23,6 +23,15 @@ sealed class DeleteAccountResponseJson {
@Serializable
data class Invalid(
@SerialName("validationErrors")
val validationErrors: Map<String, List<String?>>?,
) : DeleteAccountResponseJson()
private val validationErrors: Map<String, List<String?>>?,
) : DeleteAccountResponseJson() {
/**
* A human readable error message.
*/
val message: String?
get() = validationErrors
?.values
?.firstOrNull()
?.firstOrNull()
}
}

View File

@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.manager.util.isSso
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
@@ -51,7 +52,9 @@ class AuthRequestManagerImpl(
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
while (currentCoroutineContext().isActive) {
when (val result = getAuthRequests()) {
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
is AuthRequestsResult.Error -> {
emit(AuthRequestsUpdatesResult.Error(error = result.error))
}
is AuthRequestsResult.Success -> {
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
@@ -70,9 +73,8 @@ class AuthRequestManagerImpl(
email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(),
)
.getOrNull()
?: run {
emit(CreateAuthRequestResult.Error)
.getOrElse {
emit(CreateAuthRequestResult.Error(error = it))
return@flow
}
var authRequest = initialResult.authRequest
@@ -103,7 +105,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { emit(CreateAuthRequestResult.Error) },
onFailure = { emit(CreateAuthRequestResult.Error(error = it)) },
onSuccess = { updateAuthRequest ->
when {
updateAuthRequest.requestApproved -> {
@@ -182,7 +184,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { emit(AuthRequestUpdatesResult.Error) },
onFailure = { emit(AuthRequestUpdatesResult.Error(error = it)) },
onSuccess = { updateAuthRequest ->
when {
updateAuthRequest.requestApproved -> {
@@ -218,13 +220,18 @@ class AuthRequestManagerImpl(
fingerprint: String,
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
when (val authRequestsResult = getAuthRequests()) {
AuthRequestsResult.Error -> AuthRequestUpdatesResult.Error
is AuthRequestsResult.Error -> {
AuthRequestUpdatesResult.Error(error = authRequestsResult.error)
}
is AuthRequestsResult.Success -> {
authRequestsResult
.authRequests
.firstOrNull { it.fingerprint == fingerprint }
?.let { AuthRequestUpdatesResult.Update(it) }
?: AuthRequestUpdatesResult.Error
?: AuthRequestUpdatesResult.Error(
error = IllegalStateException("Could not find the auth request."),
)
}
}
}
@@ -234,30 +241,28 @@ class AuthRequestManagerImpl(
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
authRequestsService
.getAuthRequest(requestId)
.map { response ->
getFingerprintPhrase(response.publicKey).getOrNull()?.let { fingerprint ->
AuthRequest(
id = response.id,
publicKey = response.publicKey,
platform = response.platform,
ipAddress = response.ipAddress,
key = response.key,
masterPasswordHash = response.masterPasswordHash,
creationDate = response.creationDate,
responseDate = response.responseDate,
requestApproved = response.requestApproved ?: false,
originUrl = response.originUrl,
fingerprint = fingerprint,
)
}
.mapCatching { response ->
getFingerprintPhrase(response.publicKey)
.getOrThrow()
.let { fingerprint ->
AuthRequest(
id = response.id,
publicKey = response.publicKey,
platform = response.platform,
ipAddress = response.ipAddress,
key = response.key,
masterPasswordHash = response.masterPasswordHash,
creationDate = response.creationDate,
responseDate = response.responseDate,
requestApproved = response.requestApproved ?: false,
originUrl = response.originUrl,
fingerprint = fingerprint,
)
}
}
.fold(
onFailure = { AuthRequestUpdatesResult.Error },
onSuccess = { authRequest ->
authRequest
?.let { AuthRequestUpdatesResult.Update(it) }
?: AuthRequestUpdatesResult.Error
},
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
)
}
@@ -309,7 +314,7 @@ class AuthRequestManagerImpl(
}
}
.fold(
onFailure = { AuthRequestsResult.Error },
onFailure = { AuthRequestsResult.Error(error = it) },
onSuccess = { AuthRequestsResult.Success(authRequests = it) },
)
@@ -319,7 +324,7 @@ class AuthRequestManagerImpl(
publicKey: String,
isApproved: Boolean,
): AuthRequestResult {
val userId = activeUserId ?: return AuthRequestResult.Error
val userId = activeUserId ?: return AuthRequestResult.Error(error = NoActiveUserException())
return vaultSdkSource
.getAuthRequestKey(
publicKey = publicKey,
@@ -350,7 +355,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { AuthRequestResult.Error },
onFailure = { AuthRequestResult.Error(error = it) },
onSuccess = { AuthRequestResult.Success(authRequest = it) },
)
}
@@ -462,7 +467,7 @@ class AuthRequestManagerImpl(
publicKey: String,
): Result<String> {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return IllegalStateException("No active account").asFailure()
?: return NoActiveUserException().asFailure()
return authSdkSource.getUserFingerprint(
email = profile.email,
publicKey = publicKey,

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestResult()
data class Error(
val error: Throwable,
) : AuthRequestResult()
}

View File

@@ -19,7 +19,9 @@ sealed class AuthRequestUpdatesResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestUpdatesResult()
data class Error(
val error: Throwable,
) : AuthRequestUpdatesResult()
/**
* The auth request has been declined.

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestsResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestsResult()
data class Error(
val error: Throwable,
) : AuthRequestsResult()
}

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestsUpdatesResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestsUpdatesResult()
data class Error(
val error: Throwable,
) : AuthRequestsUpdatesResult()
}

View File

@@ -23,7 +23,9 @@ sealed class CreateAuthRequestResult {
/**
* There was a generic error getting the user's auth requests.
*/
data object Error : CreateAuthRequestResult()
data class Error(
val error: Throwable,
) : CreateAuthRequestResult()
/**
* The auth request has been declined.

View File

@@ -99,6 +99,8 @@ import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
@@ -467,7 +469,7 @@ class AuthRepositoryImpl(
masterPassword: String,
): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error(message = null)
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
mutableHasPendingAccountDeletionStateFlow.value = true
return authSdkSource
.hashPassword(
@@ -501,18 +503,13 @@ class AuthRepositoryImpl(
fold(
onFailure = {
clearPendingAccountDeletion()
DeleteAccountResult.Error(message = null)
DeleteAccountResult.Error(error = it, message = null)
},
onSuccess = { response ->
when (response) {
is DeleteAccountResponseJson.Invalid -> {
clearPendingAccountDeletion()
DeleteAccountResult.Error(
message = response.validationErrors
?.values
?.firstOrNull()
?.firstOrNull(),
)
DeleteAccountResult.Error(message = response.message, error = null)
}
DeleteAccountResponseJson.Success -> {
@@ -524,8 +521,10 @@ class AuthRepositoryImpl(
)
override suspend fun createNewSsoUser(): NewSsoUserResult {
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
val account = authDiskSource.userState?.activeAccount
?: return NewSsoUserResult.Failure(error = NoActiveUserException())
val orgIdentifier = rememberedOrgIdentifier
?: return NewSsoUserResult.Failure(error = MissingPropertyException("OrgIdentifier"))
val userId = account.profile.userId
return organizationService
.getOrganizationAutoEnrollStatus(orgIdentifier)
@@ -577,7 +576,7 @@ class AuthRepositoryImpl(
}
.fold(
onSuccess = { NewSsoUserResult.Success },
onFailure = { NewSsoUserResult.Failure },
onFailure = { NewSsoUserResult.Failure(error = it) },
)
}
@@ -586,10 +585,13 @@ class AuthRepositoryImpl(
asymmetricalKey: String,
): LoginResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return LoginResult.Error(errorMessage = null)
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
val userId = profile.userId
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return LoginResult.Error(errorMessage = null)
?: return LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Private Key"),
)
checkForVaultUnlockError(
onVaultUnlockError = { error ->
@@ -639,7 +641,7 @@ class AuthRepositoryImpl(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
else -> LoginResult.Error(errorMessage = null)
else -> LoginResult.Error(errorMessage = null, error = throwable)
}
},
onSuccess = { it },
@@ -688,7 +690,10 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
?: LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Identity Token Auth Model"),
)
override suspend fun login(
email: String,
@@ -708,7 +713,10 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
?: LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Identity Token Auth Model"),
)
override suspend fun login(
email: String,
@@ -767,7 +775,7 @@ class AuthRepositoryImpl(
override suspend fun requestOneTimePasscode(): RequestOtpResult =
accountsService.requestOneTimePasscode()
.fold(
onFailure = { RequestOtpResult.Error(it.message) },
onFailure = { RequestOtpResult.Error(message = it.message, error = it) },
onSuccess = { RequestOtpResult.Success },
)
@@ -777,7 +785,7 @@ class AuthRepositoryImpl(
passcode = oneTimePasscode,
)
.fold(
onFailure = { VerifyOtpResult.NotVerified(it.message) },
onFailure = { VerifyOtpResult.NotVerified(errorMessage = it.message, error = it) },
onSuccess = { VerifyOtpResult.Verified },
)
@@ -785,21 +793,27 @@ class AuthRepositoryImpl(
resendEmailRequestJson
?.let { jsonRequest ->
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
?: ResendEmailResult.Error(
message = null,
error = MissingPropertyException("Resend Email Request"),
)
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
resendNewDeviceOtpRequestJson
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
?: ResendEmailResult.Error(
message = null,
error = MissingPropertyException("Resend New Device OTP Request"),
)
override fun switchAccount(userId: String): SwitchAccountResult {
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
@@ -901,7 +915,10 @@ class AuthRepositoryImpl(
is RegisterResponseJson.CaptchaRequired -> {
it.validationErrors.captchaKeys.firstOrNull()
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
?: RegisterResult.Error(errorMessage = null)
?: RegisterResult.Error(
errorMessage = null,
error = MissingPropertyException("Captcha ID"),
)
}
is RegisterResponseJson.Success -> {
@@ -910,11 +927,11 @@ class AuthRepositoryImpl(
}
is RegisterResponseJson.Invalid -> {
RegisterResult.Error(errorMessage = it.message)
RegisterResult.Error(errorMessage = it.message, error = null)
}
}
},
onFailure = { RegisterResult.Error(errorMessage = null) },
onFailure = { RegisterResult.Error(errorMessage = null, error = it) },
)
}
@@ -922,11 +939,15 @@ class AuthRepositoryImpl(
return accountsService.requestPasswordHint(email).fold(
onSuccess = {
when (it) {
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(it.errorMessage)
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(
message = it.errorMessage,
error = null,
)
PasswordHintResponseJson.Success -> PasswordHintResult.Success
}
},
onFailure = { PasswordHintResult.Error(null) },
onFailure = { PasswordHintResult.Error(message = null, error = it) },
)
}
@@ -934,12 +955,12 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(error = NoActiveUserException())
val profile = activeAccount.profile
val userId = profile.userId
val userKey = authDiskSource
.getUserKey(userId = userId)
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(error = MissingPropertyException("User Key"))
val keyConnectorUrl = organizations
.find {
it.shouldUseKeyConnector &&
@@ -947,7 +968,9 @@ class AuthRepositoryImpl(
it.type != OrganizationType.ADMIN
}
?.keyConnectorUrl
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(
error = MissingPropertyException("Key Connector URL"),
)
return keyConnectorManager
.migrateExistingUserToKeyConnector(
userId = userId,
@@ -965,7 +988,7 @@ class AuthRepositoryImpl(
settingsRepository.setDefaultsIfNecessary(userId = userId)
}
.fold(
onFailure = { RemovePasswordResult.Error },
onFailure = { RemovePasswordResult.Error(error = it) },
onSuccess = { RemovePasswordResult.Success },
)
}
@@ -978,7 +1001,7 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return ResetPasswordResult.Error
?: return ResetPasswordResult.Error(error = NoActiveUserException())
val currentPasswordHash = currentPassword?.let { password ->
authSdkSource
.hashPassword(
@@ -988,7 +1011,7 @@ class AuthRepositoryImpl(
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
.fold(
onFailure = { return ResetPasswordResult.Error },
onFailure = { return ResetPasswordResult.Error(error = it) },
onSuccess = { it },
)
}
@@ -1034,7 +1057,7 @@ class AuthRepositoryImpl(
// Return the success.
ResetPasswordResult.Success
},
onFailure = { ResetPasswordResult.Error },
onFailure = { ResetPasswordResult.Error(error = it) },
)
}
@@ -1047,7 +1070,7 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return SetPasswordResult.Error
?: return SetPasswordResult.Error(error = NoActiveUserException())
val userId = activeAccount.profile.userId
// Update the saved master password hash.
@@ -1058,7 +1081,7 @@ class AuthRepositoryImpl(
kdf = activeAccount.profile.toSdkParams(),
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
.getOrElse { return@setPassword SetPasswordResult.Error }
.getOrElse { return@setPassword SetPasswordResult.Error(error = it) }
return when (activeAccount.profile.forcePasswordResetReason) {
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION -> {
@@ -1108,7 +1131,7 @@ class AuthRepositoryImpl(
}
}
.flatMap {
when (vaultRepository.unlockVaultWithMasterPassword(password)) {
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
is VaultUnlockResult.Success -> {
enrollUserInPasswordReset(
userId = userId,
@@ -1117,12 +1140,9 @@ class AuthRepositoryImpl(
)
}
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
IllegalStateException("Failed to unlock vault").asFailure()
is VaultUnlockError -> {
(result.error ?: IllegalStateException("Failed to unlock vault"))
.asFailure()
}
}
}
@@ -1132,7 +1152,7 @@ class AuthRepositoryImpl(
this.organizationIdentifier = null
}
.fold(
onFailure = { SetPasswordResult.Error },
onFailure = { SetPasswordResult.Error(error = it) },
onSuccess = { SetPasswordResult.Success },
)
}
@@ -1167,7 +1187,7 @@ class AuthRepositoryImpl(
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
onFailure = { OrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
@@ -1182,7 +1202,7 @@ class AuthRepositoryImpl(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun prevalidateSso(
@@ -1195,19 +1215,19 @@ class AuthRepositoryImpl(
onSuccess = {
when (it) {
is PrevalidateSsoResponseJson.Error -> {
PrevalidateSsoResult.Failure(message = it.message)
PrevalidateSsoResult.Failure(message = it.message, error = null)
}
is PrevalidateSsoResponseJson.Success -> {
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure()
PrevalidateSsoResult.Failure(error = MissingPropertyException("Token"))
} else {
PrevalidateSsoResult.Success(token = it.token)
}
}
}
},
onFailure = { PrevalidateSsoResult.Failure() },
onFailure = { PrevalidateSsoResult.Failure(error = it) },
)
override fun setSsoCallbackResult(result: SsoCallbackResult) {
@@ -1221,15 +1241,15 @@ class AuthRepositoryImpl(
deviceId = authDiskSource.uniqueAppId,
)
.fold(
onFailure = { KnownDeviceResult.Error },
onSuccess = { KnownDeviceResult.Success(it) },
onFailure = { KnownDeviceResult.Error(error = it) },
onSuccess = { KnownDeviceResult.Success(isKnownDevice = it) },
)
override suspend fun getPasswordBreachCount(password: String): BreachCountResult =
haveIBeenPwnedService
.getPasswordBreachCount(password)
.fold(
onFailure = { BreachCountResult.Error },
onFailure = { BreachCountResult.Error(error = it) },
onSuccess = { BreachCountResult.Success(it) },
)
@@ -1249,11 +1269,11 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { PasswordStrengthResult.Success(passwordStrength = it) },
onFailure = { PasswordStrengthResult.Error },
onFailure = { PasswordStrengthResult.Error(error = it) },
)
override suspend fun validatePassword(password: String): ValidatePasswordResult {
val userId = activeUserId ?: return ValidatePasswordResult.Error
val userId = activeUserId ?: return ValidatePasswordResult.Error(NoActiveUserException())
return authDiskSource
.getMasterPasswordHash(userId = userId)
?.let { masterPasswordHash ->
@@ -1265,13 +1285,13 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { ValidatePasswordResult.Success(isValid = it) },
onFailure = { ValidatePasswordResult.Error },
onFailure = { ValidatePasswordResult.Error(error = it) },
)
}
?: run {
val encryptedKey = authDiskSource
.getUserKey(userId)
?: return ValidatePasswordResult.Error
?: return ValidatePasswordResult.Error(MissingPropertyException("UserKey"))
vaultSdkSource
.validatePasswordUserKey(
userId = userId,
@@ -1301,10 +1321,12 @@ class AuthRepositoryImpl(
.userState
?.activeAccount
?.profile
?: return ValidatePinResult.Error
?: return ValidatePinResult.Error(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error
?: return ValidatePinResult.Error(
error = MissingPropertyException("Pin Protected User Key"),
)
return vaultSdkSource
.validatePin(
userId = activeAccount.userId,
@@ -1313,7 +1335,7 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },
onFailure = { ValidatePinResult.Error },
onFailure = { ValidatePinResult.Error(error = it) },
)
}
@@ -1339,7 +1361,10 @@ class AuthRepositoryImpl(
onSuccess = {
when (it) {
is SendVerificationEmailResponseJson.Invalid -> {
SendVerificationEmailResult.Error(it.message)
SendVerificationEmailResult.Error(
errorMessage = it.message,
error = null,
)
}
is SendVerificationEmailResponseJson.Success -> {
@@ -1347,9 +1372,7 @@ class AuthRepositoryImpl(
}
}
},
onFailure = {
SendVerificationEmailResult.Error(null)
},
onFailure = { SendVerificationEmailResult.Error(errorMessage = null, error = it) },
)
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
@@ -1365,15 +1388,13 @@ class AuthRepositoryImpl(
when (val json = it) {
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
is VerifyEmailTokenResponseJson.Invalid -> {
EmailTokenResult.Error(json.message)
EmailTokenResult.Error(message = json.message, error = null)
}
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
}
},
onFailure = {
EmailTokenResult.Error(message = null)
},
onFailure = { EmailTokenResult.Error(message = null, error = it) },
)
}
@@ -1645,7 +1666,10 @@ class AuthRepositoryImpl(
LoginResult.UnofficialServerError
}
else -> LoginResult.Error(errorMessage = null)
else -> LoginResult.Error(
errorMessage = null,
error = throwable,
)
}
},
onSuccess = { loginResponse ->
@@ -1681,6 +1705,7 @@ class AuthRepositoryImpl(
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
LoginResult.Error(
errorMessage = loginResponse.errorMessage,
error = null,
)
}
}
@@ -1883,7 +1908,7 @@ class AuthRepositoryImpl(
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onFailure = { VaultUnlockResult.GenericError(error = it) },
onSuccess = { it },
)
} else {
@@ -1923,7 +1948,7 @@ class AuthRepositoryImpl(
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onFailure = { VaultUnlockResult.GenericError(error = it) },
onSuccess = { it },
)
}

View File

@@ -12,5 +12,8 @@ sealed class BreachCountResult {
/**
* There was an error determining if the password has been breached.
*/
data object Error : BreachCountResult()
data class Error(
val error: Throwable,
val message: String? = null,
) : BreachCountResult()
}

View File

@@ -12,5 +12,8 @@ sealed class DeleteAccountResult {
/**
* There was an error deleting the account.
*/
data class Error(val message: String?) : DeleteAccountResult()
data class Error(
val message: String?,
val error: Throwable?,
) : DeleteAccountResult()
}

View File

@@ -18,5 +18,8 @@ sealed class EmailTokenResult {
/**
* There was an error validating the token.
*/
data class Error(val message: String?) : EmailTokenResult()
data class Error(
val message: String?,
val error: Throwable?,
) : EmailTokenResult()
}

View File

@@ -12,5 +12,7 @@ sealed class KnownDeviceResult {
/**
* There was an error determining if this is a known device.
*/
data object Error : KnownDeviceResult()
data class Error(
val error: Throwable,
) : KnownDeviceResult()
}

View File

@@ -22,7 +22,10 @@ sealed class LoginResult {
/**
* There was an error logging in.
*/
data class Error(val errorMessage: String?) : LoginResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : LoginResult()
/**
* There was an error while logging into an unofficial Bitwarden server.

View File

@@ -8,9 +8,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
* the necessary `message` if applicable.
*/
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)
is VaultUnlockResult.AuthenticationError -> {
LoginResult.Error(errorMessage = this.message, error = this.error)
}
is VaultUnlockResult.BiometricDecodingError,
is VaultUnlockResult.GenericError,
is VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null, error = this.error)
}

View File

@@ -12,5 +12,7 @@ sealed class NewSsoUserResult {
/**
* There was an error while truing to create the new user.
*/
data object Failure : NewSsoUserResult()
data class Failure(
val error: Throwable,
) : NewSsoUserResult()
}

View File

@@ -22,5 +22,7 @@ sealed class OrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : OrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : OrganizationDomainSsoDetailsResult()
}

View File

@@ -13,5 +13,8 @@ sealed class PasswordHintResult {
/**
* There was an error.
*/
data class Error(val message: String?) : PasswordHintResult()
data class Error(
val message: String?,
val error: Throwable?,
) : PasswordHintResult()
}

View File

@@ -16,5 +16,7 @@ sealed class PasswordStrengthResult {
/**
* There was an error determining the password strength.
*/
data object Error : PasswordStrengthResult()
data class Error(
val error: Throwable,
) : PasswordStrengthResult()
}

View File

@@ -16,5 +16,6 @@ sealed class PrevalidateSsoResult {
*/
data class Failure(
val message: String? = null,
val error: Throwable?,
) : PrevalidateSsoResult()
}

View File

@@ -23,7 +23,10 @@ sealed class RegisterResult {
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : RegisterResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : RegisterResult()
/**
* Password hash was found in a data breach.

View File

@@ -12,5 +12,7 @@ sealed class RemovePasswordResult {
/**
* There was an error removing the password.
*/
data object Error : RemovePasswordResult()
data class Error(
val error: Throwable,
) : RemovePasswordResult()
}

View File

@@ -13,5 +13,8 @@ sealed class RequestOtpResult {
/**
* Represents a failure to send the one-time passcode.
*/
data class Error(val message: String?) : RequestOtpResult()
data class Error(
val message: String?,
val error: Throwable,
) : RequestOtpResult()
}

View File

@@ -13,5 +13,8 @@ sealed class ResendEmailResult {
/**
* There was an error.
*/
data class Error(val message: String?) : ResendEmailResult()
data class Error(
val message: String?,
val error: Throwable,
) : ResendEmailResult()
}

View File

@@ -12,5 +12,7 @@ sealed class ResetPasswordResult {
/**
* There was an error resetting the password.
*/
data object Error : ResetPasswordResult()
data class Error(
val error: Throwable,
) : ResetPasswordResult()
}

View File

@@ -18,5 +18,8 @@ sealed class SendVerificationEmailResult {
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : SendVerificationEmailResult()
}

View File

@@ -12,5 +12,7 @@ sealed class SetPasswordResult {
/**
* There was an error setting the password.
*/
data object Error : SetPasswordResult()
data class Error(
val error: Throwable,
) : SetPasswordResult()
}

View File

@@ -14,5 +14,7 @@ sealed class UserFingerprintResult {
/**
* There was an error getting the user fingerprint.
*/
data object Error : UserFingerprintResult()
data class Error(
val error: Throwable,
) : UserFingerprintResult()
}

View File

@@ -15,5 +15,7 @@ sealed class ValidatePasswordResult {
/**
* There was an error determining if the validity of the password.
*/
data object Error : ValidatePasswordResult()
data class Error(
val error: Throwable,
) : ValidatePasswordResult()
}

View File

@@ -14,5 +14,7 @@ sealed class ValidatePinResult {
/**
* There was an error determining if the validity of the PIN.
*/
data object Error : ValidatePinResult()
data class Error(
val error: Throwable,
) : ValidatePinResult()
}

View File

@@ -18,5 +18,7 @@ sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : VerifiedOrganizationDomainSsoDetailsResult()
}

View File

@@ -13,5 +13,8 @@ sealed class VerifyOtpResult {
/**
* Represents a failure to verify the one-time passcode.
*/
data class NotVerified(val errorMessage: String?) : VerifyOtpResult()
data class NotVerified(
val errorMessage: String?,
val error: Throwable,
) : VerifyOtpResult()
}

View File

@@ -32,6 +32,11 @@ class BitwardenAccessibilityService : AccessibilityService() {
override fun onInterrupt() = Unit
override fun onCreate() {
super.onCreate()
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
}
override fun onUnbind(intent: Intent?): Boolean {
return super
.onUnbind(intent)

View File

@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.autofill.accessibility.util
import android.content.Context
import android.provider.Settings
import com.x8bit.bitwarden.LEGACY_ACCESSIBILITY_SERVICE_NAME
import com.x8bit.bitwarden.LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.util.containsAnyTerms
/**
* Helper method to determine if the [BitwardenAccessibilityService] is enabled.
@@ -11,16 +13,25 @@ import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilitySer
val Context.isAccessibilityServiceEnabled: Boolean
get() {
val appContext = this.applicationContext
val accessibilityServiceName = appContext
.packageName
?.let { "$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME" }
?: return false
val packageName = appContext.packageName
val accessibilityServiceName = packageName?.let {
"$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME"
}
val shortAccessibilityServiceName = packageName.let {
"$it/$LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME"
}
return Settings
.Secure
.getString(
appContext.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
)
?.contains(accessibilityServiceName)
?.containsAnyTerms(
terms = listOfNotNull(
accessibilityServiceName,
shortAccessibilityServiceName,
),
ignoreCase = true,
)
?: false
}

View File

@@ -267,7 +267,7 @@ class Fido2ProviderProcessorImpl(
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(cipherViews)
return when (result) {
DecryptFido2CredentialAutofillViewResult.Error -> {
is DecryptFido2CredentialAutofillViewResult.Error -> {
throw GetCredentialUnknownException("Error decrypting credentials.")
}

View File

@@ -0,0 +1,8 @@
package com.x8bit.bitwarden.data.platform.error
/**
* An exception indicating that a required property was missing.
*/
class MissingPropertyException(
propertyName: String,
) : IllegalStateException("Missing the required $propertyName property")

View File

@@ -0,0 +1,6 @@
package com.x8bit.bitwarden.data.platform.error
/**
* An exception indicating that there is currently no active user when one is required.
*/
class NoActiveUserException : IllegalStateException("No current active user!")

View File

@@ -158,7 +158,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowAddLoginCoachMarkFlow()
.map { it ?: true }
.map { it != false }
.mapFalseIfAnyLoginCiphersAvailable()
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
@@ -172,11 +172,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowGeneratorCoachMarkFlow()
.map { it ?: true }
.map { it != false }
.mapFalseIfAnyLoginCiphersAvailable()
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { shouldShow, featureFlagEnabled ->
// If the feature flag is off always return true so observers know
// the card has not been shown.
shouldShow && featureFlagEnabled
}
.distinctUntilChanged()
@@ -298,8 +300,8 @@ class FirstTimeActionManagerImpl @Inject constructor(
}
/**
* If there are any existing "Login" type ciphers then we'll map the current value
* of the receiver Flow to `false`.
* If there are any existing "Login" type ciphers then we'll map the current value
* of the receiver Flow to `false`.
*/
@OptIn(ExperimentalCoroutinesApi::class)
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
@@ -310,8 +312,10 @@ class FirstTimeActionManagerImpl @Inject constructor(
combine(
flow = this,
flow2 = vaultDiskSource.getCiphers(activeUserId),
) { currentValue, ciphers ->
currentValue && ciphers.none { it.login != null }
) { receiverCurrentValue, ciphers ->
receiverCurrentValue && ciphers.none {
it.login != null && it.organizationId == null
}
}
}
.distinctUntilChanged()

View File

@@ -5,6 +5,7 @@ import android.security.KeyChain
import android.security.KeyChainException
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
import timber.log.Timber
import java.io.IOException
@@ -24,7 +25,7 @@ class KeyManagerImpl(
private val context: Context,
) : KeyManager {
@Suppress("CyclomaticComplexMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun importMutualTlsCertificate(
key: ByteArray,
alias: String,
@@ -35,28 +36,29 @@ class KeyManagerImpl(
.inputStream()
.use { stream ->
try {
KeyStore.getInstance(KEYSTORE_TYPE_PKCS12)
KeyStore
.getInstance(KEYSTORE_TYPE_PKCS12)
.also { it.load(stream, password.toCharArray()) }
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to load PKCS12 bytes")
return ImportPrivateKeyResult.Error.UnsupportedKey
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
} catch (e: IOException) {
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
return when (e.cause) {
is UnrecoverableKeyException -> {
ImportPrivateKeyResult.Error.UnrecoverableKey
ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
}
else -> {
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
}
}
} catch (e: CertificateException) {
Timber.Forest.e(e, "Unable to load certificate chain")
return ImportPrivateKeyResult.Error.InvalidCertificateChain
return ImportPrivateKeyResult.Error.InvalidCertificateChain(throwable = e)
} catch (e: NoSuchAlgorithmException) {
Timber.Forest.e(e, "Cryptographic algorithm not supported")
return ImportPrivateKeyResult.Error.UnsupportedKey
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
}
}
@@ -64,22 +66,29 @@ class KeyManagerImpl(
val internalAlias = pkcs12KeyStore.aliases()
?.takeIf { it.hasMoreElements() }
?.nextElement()
?: return ImportPrivateKeyResult.Error.UnsupportedKey
?: return ImportPrivateKeyResult.Error.UnsupportedKey(
throwable = MissingPropertyException("Internal Alias"),
)
// Step 3: Extract PrivateKey and X.509 certificate from the KeyStore and verify
// certificate alias.
val privateKey = try {
pkcs12KeyStore.getKey(internalAlias, password.toCharArray())
?: return ImportPrivateKeyResult.Error.UnrecoverableKey
pkcs12KeyStore
.getKey(internalAlias, password.toCharArray())
?: return ImportPrivateKeyResult.Error.UnrecoverableKey(
throwable = MissingPropertyException("Private Key"),
)
} catch (e: UnrecoverableKeyException) {
Timber.Forest.e(e, "Failed to get private key")
return ImportPrivateKeyResult.Error.UnrecoverableKey
return ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
}
val certChain: Array<Certificate> = pkcs12KeyStore
.getCertificateChain(internalAlias)
?.takeUnless { it.isEmpty() }
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain(
throwable = MissingPropertyException("Certificate Chain"),
)
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
// does not exists.
@@ -92,7 +101,7 @@ class KeyManagerImpl(
setKeyEntry(alias, privateKey, null, certChain)
} catch (e: KeyStoreException) {
Timber.Forest.e(e, "Failed to import key into Android KeyStore")
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
}
}
return ImportPrivateKeyResult.Success(alias)

View File

@@ -44,12 +44,13 @@ sealed class FlagKey<out T : Any> {
AnonAddySelfHostAlias,
SimpleLoginSelfHostAlias,
ChromeAutofill,
MobileErrorReporting,
)
}
}
/**
* Data object holding the key for syncing with the Bitwarden Authenticator app.
* Data object holding the key for syncing with the Bitwarden Authenticator app.
*/
data object AuthenticatorSync : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-bwa-sync"
@@ -66,6 +67,15 @@ sealed class FlagKey<out T : Any> {
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the key for syncing with the Bitwarden Authenticator app.
*/
data object MobileErrorReporting : FlagKey<Boolean>() {
override val keyName: String = "mobile-error-reporting"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the feature flag key for the Onboarding Carousel feature.
*/
@@ -218,7 +228,7 @@ sealed class FlagKey<out T : Any> {
* autofill.
*/
data object ChromeAutofill : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-chrome-autofill"
override val keyName: String = "android-chrome-autofill"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}

View File

@@ -16,30 +16,44 @@ sealed class ImportPrivateKeyResult {
* Represents a generic error during the import process.
*/
sealed class Error : ImportPrivateKeyResult() {
/**
* The underlying error.
*/
abstract val throwable: Throwable?
/**
* Indicates that the provided key is unrecoverable or the password is incorrect.
*/
data object UnrecoverableKey : Error()
data class UnrecoverableKey(
override val throwable: Throwable,
) : Error()
/**
* Indicates that the certificate chain associated with the key is invalid.
*/
data object InvalidCertificateChain : Error()
data class InvalidCertificateChain(
override val throwable: Throwable,
) : Error()
/**
* Indicates that the specified alias is already in use.
*/
data object DuplicateAlias : Error()
data object DuplicateAlias : Error() {
override val throwable: Throwable? = null
}
/**
* Indicates that an error occurred during the key store operation.
*/
data object KeyStoreOperationFailed : Error()
data class KeyStoreOperationFailed(
override val throwable: Throwable,
) : Error()
/**
* Indicates the provided key is not supported.
*/
data object UnsupportedKey : Error()
data class UnsupportedKey(
override val throwable: Throwable,
) : Error()
}
}

View File

@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
import com.x8bit.bitwarden.data.platform.manager.PushManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.ZonedDateTime
/**
@@ -12,6 +13,7 @@ import java.time.ZonedDateTime
* Note: The data we receive is not always reliable, so everything is nullable and we validate the
* data in the [PushManager] as necessary.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
sealed class NotificationPayload {
/**
@@ -24,12 +26,12 @@ sealed class NotificationPayload {
*/
@Serializable
data class SyncCipherNotification(
@SerialName("Id") val cipherId: String?,
@SerialName("UserId") override val userId: String?,
@SerialName("OrganizationId") val organizationId: String?,
@SerialName("CollectionIds") val collectionIds: List<String>?,
@JsonNames("Id", "id") val cipherId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@JsonNames("OrganizationId", "organizationId") val organizationId: String?,
@JsonNames("CollectionIds", "collectionIds") val collectionIds: List<String>?,
@Contextual
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
) : NotificationPayload()
/**
@@ -37,10 +39,10 @@ sealed class NotificationPayload {
*/
@Serializable
data class SyncFolderNotification(
@SerialName("Id") val folderId: String?,
@SerialName("UserId") override val userId: String?,
@JsonNames("Id", "id") val folderId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@Contextual
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
) : NotificationPayload()
/**
@@ -48,9 +50,9 @@ sealed class NotificationPayload {
*/
@Serializable
data class UserNotification(
@SerialName("UserId") override val userId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@Contextual
@SerialName("Date") val date: ZonedDateTime?,
@JsonNames("Date", "date") val date: ZonedDateTime?,
) : NotificationPayload()
/**
@@ -58,10 +60,10 @@ sealed class NotificationPayload {
*/
@Serializable
data class SyncSendNotification(
@SerialName("Id") val sendId: String?,
@SerialName("UserId") override val userId: String?,
@JsonNames("Id", "id") val sendId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@Contextual
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
) : NotificationPayload()
/**
@@ -69,7 +71,7 @@ sealed class NotificationPayload {
*/
@Serializable
data class PasswordlessRequestNotification(
@SerialName("UserId") override val userId: String?,
@SerialName("Id") val loginRequestId: String?,
@JsonNames("UserId", "userId") override val userId: String?,
@JsonNames("Id", "id") val loginRequestId: String?,
) : NotificationPayload()
}

View File

@@ -72,9 +72,9 @@ class AuthenticatorBridgeRepositoryImpl(
when (unlockResult) {
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
is VaultUnlockResult.BiometricDecodingError,
is VaultUnlockResult.GenericError,
is VaultUnlockResult.InvalidStateError,
-> {
// Not being able to unlock the user's vault with the
// decrypted unlock key is an unexpected case, but if it does
@@ -116,7 +116,10 @@ class AuthenticatorBridgeRepositoryImpl(
// Lock the user's vault if we unlocked it for this operation:
if (!isVaultAlreadyUnlocked) {
vaultRepository.lockVault(userId)
vaultRepository.lockVault(
userId = userId,
isUserInitiated = false,
)
}
SharedAccountData.Account(

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
@@ -380,12 +381,13 @@ class SettingsRepositoryImpl(
}
override suspend fun getUserFingerprint(): UserFingerprintResult {
val userId = activeUserId ?: return UserFingerprintResult.Error
val userId = activeUserId
?: return UserFingerprintResult.Error(error = NoActiveUserException())
return vaultSdkSource
.getUserFingerprint(userId)
.fold(
onFailure = { UserFingerprintResult.Error },
onFailure = { UserFingerprintResult.Error(error = it) },
onSuccess = { UserFingerprintResult.Success(it) },
)
}
@@ -492,7 +494,8 @@ class SettingsRepositoryImpl(
}
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
val userId = activeUserId ?: return BiometricsKeyResult.Error
val userId = activeUserId
?: return BiometricsKeyResult.Error(error = NoActiveUserException())
return vaultSdkSource
.getUserEncryptionKey(userId = userId)
.onSuccess { biometricsKey ->
@@ -506,7 +509,7 @@ class SettingsRepositoryImpl(
}
.fold(
onSuccess = { BiometricsKeyResult.Success },
onFailure = { BiometricsKeyResult.Error },
onFailure = { BiometricsKeyResult.Error(error = it) },
)
}

View File

@@ -12,5 +12,7 @@ sealed class BiometricsKeyResult {
/**
* Generic error while setting up the biometrics key.
*/
data object Error : BiometricsKeyResult()
data class Error(
val error: Throwable,
) : BiometricsKeyResult()
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.util
import android.os.Build
import com.x8bit.bitwarden.BuildConfig
/**
@@ -7,3 +8,55 @@ import com.x8bit.bitwarden.BuildConfig
*/
val isFdroid: Boolean
get() = BuildConfig.FLAVOR == "fdroid"
/**
* A string that represents a displayable app version.
*/
val versionData: String
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
/**
* A string that represents device data.
*/
val deviceData: String get() = "$deviceBrandModel $osInfo $buildInfo"
/**
* A string representing the CI information if available.
*/
val ciBuildInfo: String? get() = BuildConfig.CI_INFO.takeUnless { it.isBlank() }
/**
* A string representing the build flavor or blank if it is the standard configuration.
*/
private val buildFlavorName: String
get() = when (BuildConfig.FLAVOR) {
"standard" -> ""
else -> "-${BuildConfig.FLAVOR}"
}
/**
* A string representing the build type.
*/
private val buildTypeName: String
get() = when (BuildConfig.BUILD_TYPE) {
"debug" -> "dev"
"release" -> "prod"
else -> BuildConfig.BUILD_TYPE
}
/**
* A string representing the device brand and model.
*/
private val deviceBrandModel: String get() = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
/**
* A string representing the operating system information.
*/
private val osInfo: String get() = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
/**
* A string representing the build information.
*/
private val buildInfo: String
get() = "\uD83D\uDCE6 $buildTypeName" +
buildFlavorName.takeUnless { it.isBlank() }?.let { " $it" }.orEmpty()

View File

@@ -130,7 +130,7 @@ class GeneratorRepositoryImpl(
}
GeneratedPasswordResult.Success(generatedPassword)
},
onFailure = { GeneratedPasswordResult.InvalidRequest },
onFailure = { GeneratedPasswordResult.InvalidRequest(error = it) },
)
override suspend fun generatePassphrase(
@@ -149,7 +149,7 @@ class GeneratorRepositoryImpl(
}
GeneratedPassphraseResult.Success(generatedPassphrase)
},
onFailure = { GeneratedPassphraseResult.InvalidRequest },
onFailure = { GeneratedPassphraseResult.InvalidRequest(error = it) },
)
override suspend fun generatePlusAddressedEmail(
@@ -161,7 +161,7 @@ class GeneratorRepositoryImpl(
GeneratedPlusAddressedUsernameResult.Success(generatedEmail)
},
onFailure = {
GeneratedPlusAddressedUsernameResult.InvalidRequest
GeneratedPlusAddressedUsernameResult.InvalidRequest(error = it)
},
)
@@ -174,7 +174,7 @@ class GeneratorRepositoryImpl(
GeneratedCatchAllUsernameResult.Success(generatedEmail)
},
onFailure = {
GeneratedCatchAllUsernameResult.InvalidRequest
GeneratedCatchAllUsernameResult.InvalidRequest(error = it)
},
)
@@ -187,7 +187,7 @@ class GeneratorRepositoryImpl(
GeneratedRandomWordUsernameResult.Success(generatedUsername)
},
onFailure = {
GeneratedRandomWordUsernameResult.InvalidRequest
GeneratedRandomWordUsernameResult.InvalidRequest(error = it)
},
)
@@ -200,7 +200,7 @@ class GeneratorRepositoryImpl(
GeneratedForwardedServiceUsernameResult.Success(generatedEmail)
},
onFailure = {
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message, error = it)
},
)
}

View File

@@ -15,5 +15,5 @@ sealed class GeneratedCatchAllUsernameResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedCatchAllUsernameResult()
data class InvalidRequest(val error: Throwable) : GeneratedCatchAllUsernameResult()
}

View File

@@ -14,5 +14,8 @@ sealed class GeneratedForwardedServiceUsernameResult {
/**
* There was an error during the operation.
*/
data class InvalidRequest(val message: String?) : GeneratedForwardedServiceUsernameResult()
data class InvalidRequest(
val message: String?,
val error: Throwable,
) : GeneratedForwardedServiceUsernameResult()
}

View File

@@ -12,5 +12,5 @@ sealed class GeneratedPassphraseResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPassphraseResult()
data class InvalidRequest(val error: Throwable) : GeneratedPassphraseResult()
}

View File

@@ -13,5 +13,5 @@ sealed class GeneratedPasswordResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPasswordResult()
data class InvalidRequest(val error: Throwable) : GeneratedPasswordResult()
}

View File

@@ -14,5 +14,5 @@ sealed class GeneratedPlusAddressedUsernameResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPlusAddressedUsernameResult()
data class InvalidRequest(val error: Throwable) : GeneratedPlusAddressedUsernameResult()
}

View File

@@ -15,5 +15,5 @@ sealed class GeneratedRandomWordUsernameResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedRandomWordUsernameResult()
data class InvalidRequest(val error: Throwable) : GeneratedRandomWordUsernameResult()
}

View File

@@ -169,7 +169,10 @@ class VaultSdkSourceImpl(
InitializeCryptoResult.Success
} catch (exception: BitwardenException) {
// The only truly expected error from the SDK is an incorrect key/password.
InitializeCryptoResult.AuthenticationError(message = exception.message)
InitializeCryptoResult.AuthenticationError(
message = exception.message,
error = exception,
)
}
}
@@ -187,6 +190,7 @@ class VaultSdkSourceImpl(
// The only truly expected error from the SDK is for incorrect keys.
InitializeCryptoResult.AuthenticationError(
message = exception.message,
error = exception,
)
}
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.vault.CipherView
/**
* Models the result of querying for ciphers with FIDO 2 credentials.
*/
sealed class FindFido2CredentialsResult {
/**
* Indicates the query was executed successfully.
*/
data class Success(val cipherViews: List<CipherView>) : FindFido2CredentialsResult()
/**
* Indicates the query was not executed successfully.
*/
data object Error : FindFido2CredentialsResult()
}

View File

@@ -15,5 +15,6 @@ sealed class InitializeCryptoResult {
*/
data class AuthenticationError(
val message: String? = null,
val error: Throwable,
) : InitializeCryptoResult()
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
/**
* Models the result of saving a FIDO 2 credential.
*/
sealed class SaveCredentialResult {
/**
* Indicates the credential has been saved.
*/
data object Success : SaveCredentialResult()
/**
* Indicates the credential was not saved.
*/
data object Error : SaveCredentialResult()
}

View File

@@ -6,6 +6,7 @@ import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.Cipher
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
@@ -49,7 +50,8 @@ class CipherManagerImpl(
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -58,7 +60,7 @@ class CipherManagerImpl(
.flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) }
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
.fold(
onFailure = { CreateCipherResult.Error },
onFailure = { CreateCipherResult.Error(error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
@@ -70,7 +72,8 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -91,7 +94,7 @@ class CipherManagerImpl(
)
}
.fold(
onFailure = { CreateCipherResult.Error },
onFailure = { CreateCipherResult.Error(error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
@@ -100,13 +103,14 @@ class CipherManagerImpl(
}
override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
val userId = activeUserId
?: return DeleteCipherResult.Error(error = NoActiveUserException())
return ciphersService
.hardDeleteCipher(cipherId = cipherId)
.onSuccess { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
onFailure = { DeleteCipherResult.Error(error = it) },
)
}
@@ -114,7 +118,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
val userId = activeUserId
?: return DeleteCipherResult.Error(error = NoActiveUserException())
return cipherView
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
.flatMap { cipher ->
@@ -136,7 +141,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
onFailure = { DeleteCipherResult.Error(error = it) },
)
}
@@ -152,7 +157,7 @@ class CipherManagerImpl(
)
.fold(
onSuccess = { DeleteAttachmentResult.Success },
onFailure = { DeleteAttachmentResult.Error },
onFailure = { DeleteAttachmentResult.Error(error = it) },
)
private suspend fun deleteCipherAttachmentForResult(
@@ -160,7 +165,7 @@ class CipherManagerImpl(
attachmentId: String,
cipherView: CipherView,
): Result<Cipher> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
return ciphersService
.deleteCipherAttachment(
cipherId = cipherId,
@@ -187,7 +192,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult {
val userId = activeUserId ?: return RestoreCipherResult.Error
val userId = activeUserId
?: return RestoreCipherResult.Error(error = NoActiveUserException())
return ciphersService
.restoreCipher(cipherId = cipherId)
.onSuccess {
@@ -198,7 +204,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { RestoreCipherResult.Success },
onFailure = { RestoreCipherResult.Error },
onFailure = { RestoreCipherResult.Error(error = it) },
)
}
@@ -206,7 +212,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult {
val userId = activeUserId ?: return UpdateCipherResult.Error(errorMessage = null)
val userId = activeUserId
?: return UpdateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -221,7 +228,7 @@ class CipherManagerImpl(
.map { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
UpdateCipherResult.Error(errorMessage = response.message)
UpdateCipherResult.Error(errorMessage = response.message, error = null)
}
is UpdateCipherResponseJson.Success -> {
@@ -234,7 +241,7 @@ class CipherManagerImpl(
}
}
.fold(
onFailure = { UpdateCipherResult.Error(errorMessage = null) },
onFailure = { UpdateCipherResult.Error(errorMessage = null, error = it) },
onSuccess = { it },
)
}
@@ -245,7 +252,7 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
val userId = activeUserId ?: return ShareCipherResult.Error(error = NoActiveUserException())
return migrateAttachments(userId = userId, cipherView = cipherView)
.flatMap {
vaultSdkSource.moveToOrganization(
@@ -271,7 +278,7 @@ class CipherManagerImpl(
)
}
.fold(
onFailure = { ShareCipherResult.Error },
onFailure = { ShareCipherResult.Error(error = it) },
onSuccess = { ShareCipherResult.Success },
)
}
@@ -281,7 +288,7 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
val userId = activeUserId ?: return ShareCipherResult.Error(error = NoActiveUserException())
return ciphersService
.updateCipherCollections(
cipherId = cipherId,
@@ -301,7 +308,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { ShareCipherResult.Success },
onFailure = { ShareCipherResult.Error },
onFailure = { ShareCipherResult.Error(error = it) },
)
}
@@ -320,7 +327,7 @@ class CipherManagerImpl(
fileUri = fileUri,
)
.fold(
onFailure = { CreateAttachmentResult.Error },
onFailure = { CreateAttachmentResult.Error(error = it) },
onSuccess = { CreateAttachmentResult.Success(cipherView = it) },
)
@@ -332,7 +339,7 @@ class CipherManagerImpl(
fileName: String?,
fileUri: Uri,
): Result<CipherView> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
val attachmentView = AttachmentView(
id = null,
url = null,
@@ -404,14 +411,14 @@ class CipherManagerImpl(
)
.fold(
onSuccess = { DownloadAttachmentResult.Success(file = it) },
onFailure = { DownloadAttachmentResult.Failure },
onFailure = { DownloadAttachmentResult.Failure(error = it) },
)
private suspend fun downloadAttachmentForResult(
cipherView: CipherView,
attachmentId: String,
): Result<File> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
val cipher = cipherView
.encryptCipherAndCheckForMigration(
@@ -439,7 +446,10 @@ class CipherManagerImpl(
?: return IllegalStateException("Attachment does not have a url").asFailure()
val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) {
DownloadResult.Failure -> return IllegalStateException("Download failed").asFailure()
is DownloadResult.Failure -> {
return IllegalStateException("Download failed", result.error).asFailure()
}
is DownloadResult.Success -> result.file
}
@@ -464,30 +474,36 @@ class CipherManagerImpl(
userId: String,
cipherId: String,
): Result<Cipher> =
if (this.key == null) {
vaultSdkSource
.encryptCipher(userId = userId, cipherView = this)
.flatMap {
ciphersService.updateCipher(
cipherId = cipherId,
body = it.toEncryptedNetworkCipher(),
)
}
.flatMap { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
IllegalStateException(response.message).asFailure()
}
vaultSdkSource
.encryptCipher(userId = userId, cipherView = this)
.flatMap {
// We only migrate the cipher if the original cipher did not have a key and the
// new cipher does. This means the SDK created the key and migration is required.
if (it.key != null && this.key == null) {
ciphersService
.updateCipher(
cipherId = cipherId,
body = it.toEncryptedNetworkCipher(),
)
.flatMap { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
IllegalStateException(response.message).asFailure()
}
is UpdateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(userId = userId, cipher = response.cipher)
response.cipher.toEncryptedSdkCipher().asSuccess()
is UpdateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = response.cipher,
)
response.cipher.toEncryptedSdkCipher().asSuccess()
}
}
}
}
} else {
it.asSuccess()
}
} else {
vaultSdkSource.encryptCipher(userId = userId, cipherView = this)
}
}
private suspend fun migrateAttachments(
userId: String,

View File

@@ -44,7 +44,7 @@ class FileManagerImpl(
.getDataStream(url)
.fold(
onSuccess = { it },
onFailure = { return DownloadResult.Failure },
onFailure = { return DownloadResult.Failure(error = it) },
)
// Create a temporary file in cache to write to
@@ -66,7 +66,7 @@ class FileManagerImpl(
}
fos.flush()
} catch (e: RuntimeException) {
return@withContext DownloadResult.Failure
return@withContext DownloadResult.Failure(error = e)
}
}
}
@@ -94,7 +94,7 @@ class FileManagerImpl(
}
}
true
} catch (exception: RuntimeException) {
} catch (_: RuntimeException) {
false
}
}
@@ -111,7 +111,7 @@ class FileManagerImpl(
}
}
true
} catch (exception: RuntimeException) {
} catch (_: RuntimeException) {
false
}
}

View File

@@ -22,6 +22,11 @@ interface VaultLockManager {
*/
val vaultStateEventFlow: Flow<VaultStateEvent>
/**
* Whether the user is coming from the lock flow or not.
*/
var isFromLockFlow: Boolean
/**
* Whether or not the vault is currently locked for the given [userId].
*/
@@ -35,12 +40,12 @@ interface VaultLockManager {
/**
* Locks the vault for the user with the given [userId].
*/
fun lockVault(userId: String)
fun lockVault(userId: String, isUserInitiated: Boolean)
/**
* Locks the vault for the current user if currently unlocked.
*/
fun lockVaultForCurrentUser()
fun lockVaultForCurrentUser(isUserInitiated: Boolean)
/**
* Attempt to unlock the vault with the specified user information.

View File

@@ -16,6 +16,8 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
@@ -99,6 +101,8 @@ class VaultLockManagerImpl(
override val vaultStateEventFlow: Flow<VaultStateEvent>
get() = mutableVaultStateEventSharedFlow.asSharedFlow()
override var isFromLockFlow: Boolean = false
init {
observeAppCreationChanges()
observeAppForegroundChanges()
@@ -117,13 +121,17 @@ class VaultLockManagerImpl(
override fun isVaultUnlocking(userId: String): Boolean =
mutableVaultUnlockDataStateFlow.value.statusFor(userId) == VaultUnlockData.Status.UNLOCKING
override fun lockVault(userId: String) {
override fun lockVault(userId: String, isUserInitiated: Boolean) {
isFromLockFlow = isUserInitiated
setVaultToLocked(userId = userId)
}
override fun lockVaultForCurrentUser() {
override fun lockVaultForCurrentUser(isUserInitiated: Boolean) {
activeUserId?.let {
lockVault(it)
lockVault(
userId = it,
isUserInitiated = isUserInitiated,
)
}
}
@@ -167,7 +175,7 @@ class VaultLockManagerImpl(
.fold(
onFailure = {
incrementInvalidUnlockCount(userId = userId)
VaultUnlockResult.GenericError
VaultUnlockResult.GenericError(error = it)
},
onSuccess = { initializeCryptoResult ->
initializeCryptoResult
@@ -605,9 +613,11 @@ class VaultLockManagerImpl(
initUserCryptoMethod: InitUserCryptoMethod,
): VaultUnlockResult {
val account = authDiskSource.userState?.accounts?.get(userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Private key"),
)
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
return unlockVault(
userId = userId,

View File

@@ -14,5 +14,7 @@ sealed class DownloadResult {
/**
* The download failed.
*/
data object Failure : DownloadResult()
data class Failure(
val error: Throwable,
) : DownloadResult()
}

View File

@@ -21,6 +21,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@@ -509,11 +511,13 @@ class VaultRepositoryImpl(
): DecryptFido2CredentialAutofillViewResult {
return vaultSdkSource
.decryptFido2CredentialAutofillViews(
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error,
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error(
error = NoActiveUserException(),
),
cipherViews = cipherViewList.toTypedArray(),
)
.fold(
onFailure = { DecryptFido2CredentialAutofillViewResult.Error },
onFailure = { DecryptFido2CredentialAutofillViewResult.Error(error = it) },
onSuccess = { DecryptFido2CredentialAutofillViewResult.Success(it) },
)
}
@@ -545,36 +549,54 @@ class VaultRepositoryImpl(
)
override suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val biometricsKey = authDiskSource
.getUserBiometricUnlockKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Biometric key"),
)
val iv = authDiskSource.getUserBiometricInitVector(userId = userId)
val decryptedUserKey = iv
?.let {
try {
cipher
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
.decodeToString()
} catch (e: GeneralSecurityException) {
return VaultUnlockResult.BiometricDecodingError(error = e)
}
}
?: biometricsKey
val encryptedBiometricsKey = if (iv == null) {
// Attempting to setup an encrypted pin before unlocking, if this fails we send back
// the biometrics error and users will need to sign in another way and re-setup
// biometrics.
try {
cipher
.doFinal(biometricsKey.encodeToByteArray())
.toString(Charsets.ISO_8859_1)
} catch (e: GeneralSecurityException) {
return VaultUnlockResult.BiometricDecodingError(error = e)
}
} else {
null
}
return this
.unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
decryptedUserKey = iv
?.let {
try {
cipher
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
.decodeToString()
} catch (_: GeneralSecurityException) {
return VaultUnlockResult.BiometricDecodingError
}
}
?: biometricsKey,
decryptedUserKey = decryptedUserKey,
),
)
.also {
if (it is VaultUnlockResult.Success) {
if (iv == null) {
encryptedBiometricsKey?.let {
// If this key is present, we store it and the associated IV for future use
// since we want to migrate the user to a more secure form of biometrics.
authDiskSource.storeUserBiometricUnlockKey(
userId = userId,
biometricsKey = cipher
.doFinal(biometricsKey.encodeToByteArray())
.toString(Charsets.ISO_8859_1),
biometricsKey = it,
)
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
}
@@ -586,9 +608,12 @@ class VaultRepositoryImpl(
override suspend fun unlockVaultWithMasterPassword(
masterPassword: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val userKey = authDiskSource.getUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("User key"),
)
return unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Password(
@@ -606,9 +631,12 @@ class VaultRepositoryImpl(
override suspend fun unlockVaultWithPin(
pin: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Pin protected key"),
)
return unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
@@ -622,7 +650,8 @@ class VaultRepositoryImpl(
sendView: SendView,
fileUri: Uri?,
): CreateSendResult {
val userId = activeUserId ?: return CreateSendResult.Error(message = null)
val userId = activeUserId
?: return CreateSendResult.Error(message = null, error = NoActiveUserException())
return vaultSdkSource
.encryptSend(
userId = userId,
@@ -639,6 +668,7 @@ class VaultRepositoryImpl(
is CreateSendJsonResponse.Invalid -> {
return CreateSendResult.Error(
message = createSendResponse.firstValidationErrorMessage,
error = null,
)
}
@@ -656,7 +686,7 @@ class VaultRepositoryImpl(
)
}
.fold(
onFailure = { CreateSendResult.Error(message = null) },
onFailure = { CreateSendResult.Error(message = null, error = it) },
onSuccess = {
reviewPromptManager.registerCreateSendAction()
CreateSendResult.Success(it)
@@ -668,7 +698,11 @@ class VaultRepositoryImpl(
sendId: String,
sendView: SendView,
): UpdateSendResult {
val userId = activeUserId ?: return UpdateSendResult.Error(null)
val userId = activeUserId
?: return UpdateSendResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptSend(
userId = userId,
@@ -681,11 +715,11 @@ class VaultRepositoryImpl(
)
}
.fold(
onFailure = { UpdateSendResult.Error(errorMessage = null) },
onFailure = { UpdateSendResult.Error(errorMessage = null, error = it) },
onSuccess = { response ->
when (response) {
is UpdateSendResponseJson.Invalid -> {
UpdateSendResult.Error(errorMessage = response.message)
UpdateSendResult.Error(errorMessage = response.message, error = null)
}
is UpdateSendResponseJson.Success -> {
@@ -695,9 +729,12 @@ class VaultRepositoryImpl(
userId = userId,
send = response.send.toEncryptedSdkSend(),
)
.getOrNull()
?.let { UpdateSendResult.Success(sendView = it) }
?: UpdateSendResult.Error(errorMessage = null)
.fold(
onSuccess = { UpdateSendResult.Success(sendView = it) },
onFailure = {
UpdateSendResult.Error(errorMessage = null, error = it)
},
)
}
}
},
@@ -705,14 +742,21 @@ class VaultRepositoryImpl(
}
override suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult {
val userId = activeUserId ?: return RemovePasswordSendResult.Error(null)
val userId = activeUserId
?: return RemovePasswordSendResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return sendsService
.removeSendPassword(sendId = sendId)
.fold(
onSuccess = { response ->
when (response) {
is UpdateSendResponseJson.Invalid -> {
RemovePasswordSendResult.Error(errorMessage = response.message)
RemovePasswordSendResult.Error(
errorMessage = response.message,
error = null,
)
}
is UpdateSendResponseJson.Success -> {
@@ -722,24 +766,30 @@ class VaultRepositoryImpl(
userId = userId,
send = response.send.toEncryptedSdkSend(),
)
.getOrNull()
?.let { RemovePasswordSendResult.Success(sendView = it) }
?: RemovePasswordSendResult.Error(errorMessage = null)
.fold(
onSuccess = { RemovePasswordSendResult.Success(sendView = it) },
onFailure = {
RemovePasswordSendResult.Error(
errorMessage = null,
error = it,
)
},
)
}
}
},
onFailure = { RemovePasswordSendResult.Error(errorMessage = null) },
onFailure = { RemovePasswordSendResult.Error(errorMessage = null, error = it) },
)
}
override suspend fun deleteSend(sendId: String): DeleteSendResult {
val userId = activeUserId ?: return DeleteSendResult.Error
val userId = activeUserId ?: return DeleteSendResult.Error(error = NoActiveUserException())
return sendsService
.deleteSend(sendId)
.onSuccess { vaultDiskSource.deleteSend(userId, sendId) }
.fold(
onSuccess = { DeleteSendResult.Success },
onFailure = { DeleteSendResult.Error },
onFailure = { DeleteSendResult.Error(error = it) },
)
}
@@ -747,7 +797,8 @@ class VaultRepositoryImpl(
totpCode: String,
time: DateTime,
): GenerateTotpResult {
val userId = activeUserId ?: return GenerateTotpResult.Error
val userId = activeUserId
?: return GenerateTotpResult.Error(error = NoActiveUserException())
return vaultSdkSource.generateTotp(
time = time,
userId = userId,
@@ -760,12 +811,13 @@ class VaultRepositoryImpl(
periodSeconds = it.period.toInt(),
)
},
onFailure = { GenerateTotpResult.Error },
onFailure = { GenerateTotpResult.Error(error = it) },
)
}
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
val userId = activeUserId ?: return CreateFolderResult.Error
val userId = activeUserId
?: return CreateFolderResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptFolder(
userId = userId,
@@ -781,7 +833,7 @@ class VaultRepositoryImpl(
.flatMap { vaultSdkSource.decryptFolder(userId, it.toEncryptedSdkFolder()) }
.fold(
onSuccess = { CreateFolderResult.Success(folderView = it) },
onFailure = { CreateFolderResult.Error },
onFailure = { CreateFolderResult.Error(error = it) },
)
}
@@ -789,7 +841,10 @@ class VaultRepositoryImpl(
folderId: String,
folderView: FolderView,
): UpdateFolderResult {
val userId = activeUserId ?: return UpdateFolderResult.Error(null)
val userId = activeUserId ?: return UpdateFolderResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptFolder(
userId = userId,
@@ -814,21 +869,30 @@ class VaultRepositoryImpl(
)
.fold(
onSuccess = { UpdateFolderResult.Success(it) },
onFailure = { UpdateFolderResult.Error(errorMessage = null) },
onFailure = {
UpdateFolderResult.Error(
errorMessage = null,
error = it,
)
},
)
}
is UpdateFolderResponseJson.Invalid -> {
UpdateFolderResult.Error(response.message)
UpdateFolderResult.Error(
errorMessage = response.message,
error = null,
)
}
}
},
onFailure = { UpdateFolderResult.Error(it.message) },
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
)
}
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
val userId = activeUserId ?: return DeleteFolderResult.Error
val userId = activeUserId
?: return DeleteFolderResult.Error(error = NoActiveUserException())
return folderService
.deleteFolder(
folderId = folderId,
@@ -839,7 +903,7 @@ class VaultRepositoryImpl(
}
.fold(
onSuccess = { DeleteFolderResult.Success },
onFailure = { DeleteFolderResult.Error },
onFailure = { DeleteFolderResult.Error(error = it) },
)
}
@@ -854,7 +918,8 @@ class VaultRepositoryImpl(
}
override suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult {
val userId = activeUserId ?: return ExportVaultDataResult.Error
val userId = activeUserId
?: return ExportVaultDataResult.Error(error = NoActiveUserException())
val folders = vaultDiskSource
.getFolders(userId)
.firstOrNull()
@@ -877,7 +942,7 @@ class VaultRepositoryImpl(
)
.fold(
onSuccess = { ExportVaultDataResult.Success(it) },
onFailure = { ExportVaultDataResult.Error },
onFailure = { ExportVaultDataResult.Error(error = it) },
)
}
@@ -945,9 +1010,11 @@ class VaultRepositoryImpl(
initUserCryptoMethod: InitUserCryptoMethod,
): VaultUnlockResult {
val account = authDiskSource.userState?.accounts?.get(userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Private key"),
)
val organizationKeys = authDiskSource
.getOrganizationKeys(userId = userId)
return unlockVault(

View File

@@ -17,5 +17,5 @@ sealed class CreateAttachmentResult {
/**
* Generic error while creating an attachment.
*/
data object Error : CreateAttachmentResult()
data class Error(val error: Throwable) : CreateAttachmentResult()
}

View File

@@ -13,5 +13,5 @@ sealed class CreateCipherResult {
/**
* Generic error while creating cipher.
*/
data object Error : CreateCipherResult()
data class Error(val error: Throwable) : CreateCipherResult()
}

View File

@@ -15,5 +15,5 @@ sealed class CreateFolderResult {
/**
* Generic error while creating a folder.
*/
data object Error : CreateFolderResult()
data class Error(val error: Throwable) : CreateFolderResult()
}

View File

@@ -15,5 +15,5 @@ sealed class CreateSendResult {
/**
* Generic error while creating a send.
*/
data class Error(val message: String?) : CreateSendResult()
data class Error(val message: String?, val error: Throwable?) : CreateSendResult()
}

View File

@@ -16,5 +16,5 @@ sealed class DecryptFido2CredentialAutofillViewResult {
/**
* Generic error while decrypting credentials.
*/
data object Error : DecryptFido2CredentialAutofillViewResult()
data class Error(val error: Throwable) : DecryptFido2CredentialAutofillViewResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteAttachmentResult {
/**
* Generic error while deleting an attachment.
*/
data object Error : DeleteAttachmentResult()
data class Error(val error: Throwable) : DeleteAttachmentResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteCipherResult {
/**
* Generic error while deleting a cipher.
*/
data object Error : DeleteCipherResult()
data class Error(val error: Throwable) : DeleteCipherResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteFolderResult {
/**
* Generic error while deleting a folder.
*/
data object Error : DeleteFolderResult()
data class Error(val error: Throwable) : DeleteFolderResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteSendResult {
/**
* Generic error while deleting a send.
*/
data object Error : DeleteSendResult()
data class Error(val error: Throwable) : DeleteSendResult()
}

View File

@@ -14,5 +14,5 @@ sealed class DownloadAttachmentResult {
/**
* The attachment could not be downloaded.
*/
data object Failure : DownloadAttachmentResult()
data class Failure(val error: Throwable) : DownloadAttachmentResult()
}

View File

@@ -14,5 +14,5 @@ sealed class ExportVaultDataResult {
/**
* There was an error converting the vault data.
*/
data object Error : ExportVaultDataResult()
data class Error(val error: Throwable) : ExportVaultDataResult()
}

View File

@@ -16,5 +16,5 @@ sealed class GenerateTotpResult {
/**
* An error occurred while generating the code.
*/
data object Error : GenerateTotpResult()
data class Error(val error: Throwable) : GenerateTotpResult()
}

View File

@@ -17,5 +17,5 @@ sealed class RemovePasswordSendResult {
* Generic error while removing the password protection from a send. The optional
* [errorMessage] may be displayed directly in the UI when present.
*/
data class Error(val errorMessage: String?) : RemovePasswordSendResult()
data class Error(val errorMessage: String?, val error: Throwable?) : RemovePasswordSendResult()
}

View File

@@ -13,5 +13,5 @@ sealed class RestoreCipherResult {
/**
* Generic error while restoring a cipher.
*/
data object Error : RestoreCipherResult()
data class Error(val error: Throwable) : RestoreCipherResult()
}

View File

@@ -12,5 +12,5 @@ sealed class ShareCipherResult {
/**
* Generic error while sharing cipher.
*/
data object Error : ShareCipherResult()
data class Error(val error: Throwable) : ShareCipherResult()
}

View File

@@ -13,5 +13,5 @@ sealed class TotpCodeResult {
/**
* There was an error scanning the code.
*/
data object CodeScanningError : TotpCodeResult()
data class CodeScanningError(val error: Throwable? = null) : TotpCodeResult()
}

View File

@@ -14,5 +14,5 @@ sealed class UpdateCipherResult {
* Generic error while updating cipher. The optional [errorMessage] may be displayed directly in
* the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateCipherResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateCipherResult()
}

View File

@@ -16,5 +16,5 @@ sealed class UpdateFolderResult {
* Generic error while updating a folder. The optional [errorMessage]
* may be displayed directly in the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateFolderResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateFolderResult()
}

View File

@@ -16,5 +16,5 @@ sealed class UpdateSendResult {
* Generic error while updating a send. The optional [errorMessage] may be displayed directly
* in the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateSendResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateSendResult()
}

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