Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df8f44d77d | ||
|
|
a4eff45534 | ||
|
|
b2b12be3b0 | ||
|
|
9c77c53366 | ||
|
|
1449f165dd | ||
|
|
94216cf745 | ||
|
|
120e179fb8 | ||
|
|
f10114ee17 | ||
|
|
8a059e0fbb | ||
|
|
6263788d6a | ||
|
|
6ffb3136d4 | ||
|
|
b65b01fe3d | ||
|
|
b9c134654f | ||
|
|
d1a1342587 | ||
|
|
1ec2ac472a | ||
|
|
9894322c17 | ||
|
|
7edbf4ffc8 | ||
|
|
2b1d186611 | ||
|
|
70c49922b0 | ||
|
|
30d6a4d9eb | ||
|
|
033b2b9ba0 | ||
|
|
25aec80e4c | ||
|
|
cf3d52772d | ||
|
|
f78f303a79 | ||
|
|
02cffa01e2 | ||
|
|
c2f2a5e52f | ||
|
|
d8e19415e3 | ||
|
|
387dc2f59c | ||
|
|
ec3660a86d | ||
|
|
36fb23d467 | ||
|
|
33df456cfd | ||
|
|
7a6fe5ed5f | ||
|
|
558b10499b | ||
|
|
1fb3698ba2 | ||
|
|
89f26bbc6b | ||
|
|
bbd8615cda | ||
|
|
93132f5d7b | ||
|
|
179514ddf1 | ||
|
|
4b9cff2271 | ||
|
|
c3649a9c80 | ||
|
|
9a66b9003f | ||
|
|
34e32403b0 | ||
|
|
c2e34a8b0e | ||
|
|
d0ba4b6702 | ||
|
|
eb16025800 | ||
|
|
9f06c9a051 | ||
|
|
641122b16f | ||
|
|
fbe8708378 | ||
|
|
21c7b486ff | ||
|
|
c33728d418 | ||
|
|
a9dacd561c | ||
|
|
051e15215d | ||
|
|
fee8f58c0a | ||
|
|
cc036cf3c5 | ||
|
|
4e51517ddb | ||
|
|
3b7454961d | ||
|
|
88009e1a63 | ||
|
|
0afca29b0c | ||
|
|
46a75a2944 | ||
|
|
c099f82752 | ||
|
|
1da94bd9c8 | ||
|
|
96ce8165e9 | ||
|
|
f9b617339d | ||
|
|
58084810f3 | ||
|
|
429e62e6b5 | ||
|
|
b0b7f2afdf | ||
|
|
55f160d125 | ||
|
|
f6352f5392 | ||
|
|
ac7e90c0aa | ||
|
|
88fccfd6cd | ||
|
|
5fdf8e6045 | ||
|
|
d9907cdbeb | ||
|
|
d308f1ca3b | ||
|
|
0cdc138ba3 | ||
|
|
59d5314164 | ||
|
|
9c08a37772 | ||
|
|
b13f5356fe | ||
|
|
5f0c9725ce | ||
|
|
f951fea555 | ||
|
|
4b989b01e9 | ||
|
|
aed3ec5474 | ||
|
|
b354986199 | ||
|
|
e1983a7d66 | ||
|
|
0400d79f43 | ||
|
|
c911484632 | ||
|
|
713e441d2e | ||
|
|
d4b577732b | ||
|
|
440a410d7f | ||
|
|
37a536b138 | ||
|
|
a0aca3e837 | ||
|
|
b58c29111a | ||
|
|
b0f86ea161 | ||
|
|
93b59a75a4 | ||
|
|
54fcabaea6 | ||
|
|
0e966c0304 | ||
|
|
a363712127 | ||
|
|
4d8c665917 | ||
|
|
53bdd92e72 | ||
|
|
e51aa39ede | ||
|
|
33c82129ff | ||
|
|
7c5b8c0e9f | ||
|
|
9dc01bca1c | ||
|
|
3c7920b84c | ||
|
|
b92f3abbaf | ||
|
|
b6747a63ed | ||
|
|
41a44548d2 | ||
|
|
a79d3a0d7c | ||
|
|
f3a17709e5 | ||
|
|
ced9d33d2e | ||
|
|
23b1373f80 | ||
|
|
a80eb1f533 | ||
|
|
f657edf195 | ||
|
|
d34279dca5 | ||
|
|
954aa1112a | ||
|
|
b35a3339cb | ||
|
|
b59433debd | ||
|
|
fb2db9c652 | ||
|
|
2507f3301b | ||
|
|
bdad5e4f0a | ||
|
|
b5dcdc74d7 | ||
|
|
e2d1da02d3 | ||
|
|
8253f18312 | ||
|
|
f4a98a2031 | ||
|
|
224845cfd3 | ||
|
|
fc8c2ad67a | ||
|
|
c9d6f58563 | ||
|
|
325b557506 | ||
|
|
ce751cfc87 | ||
|
|
0f451fd4b9 | ||
|
|
b7819838b8 | ||
|
|
67c6cf6b8c | ||
|
|
d91d71333b | ||
|
|
431804ea80 | ||
|
|
7a7ab7bd0e | ||
|
|
b8cdee383b | ||
|
|
580fa02ee1 | ||
|
|
421834153d | ||
|
|
011f04e1dc | ||
|
|
3d8056704c | ||
|
|
41263f3419 | ||
|
|
9fe9210cb7 | ||
|
|
2272b10820 | ||
|
|
9d6fc73fcc | ||
|
|
73ecd67b20 | ||
|
|
e5ce3dbd32 | ||
|
|
a0a5e30f48 | ||
|
|
0eddee5816 | ||
|
|
6d2dcb73ae | ||
|
|
0d6cc91b67 | ||
|
|
ae52922698 | ||
|
|
236496e69f | ||
|
|
fe5cdb0004 | ||
|
|
f9547f158e | ||
|
|
0c75374c0f | ||
|
|
0b249d4dd4 | ||
|
|
a2bac9d368 | ||
|
|
392e429dfd | ||
|
|
50623b9b29 | ||
|
|
e7ce050324 | ||
|
|
762b574d49 | ||
|
|
d73bf6d225 | ||
|
|
c2108fdda0 | ||
|
|
2062a284e3 | ||
|
|
9164c9b946 | ||
|
|
13ddd10c40 | ||
|
|
e407acd2a7 | ||
|
|
11cdf52ec8 | ||
|
|
40a3541e8e | ||
|
|
7da13e22ad | ||
|
|
38d702b6fe | ||
|
|
df2af5459e | ||
|
|
40d68b1654 | ||
|
|
a240a4ac66 | ||
|
|
416ec3812d | ||
|
|
ff24891903 | ||
|
|
ddcbe298ac | ||
|
|
6d8f647aee | ||
|
|
a654987175 | ||
|
|
a5f960d8a1 | ||
|
|
1f707cda68 | ||
|
|
4e7f195fd2 | ||
|
|
7728e930be | ||
|
|
ab84200347 | ||
|
|
62d8824450 | ||
|
|
cf35d20adb | ||
|
|
65725b5a38 | ||
|
|
eca4777b99 | ||
|
|
066b3aba5b | ||
|
|
8e485ff26f | ||
|
|
341b66f44f | ||
|
|
19c62d3320 | ||
|
|
13ffbd7675 | ||
|
|
9af6aae699 | ||
|
|
2e562e8318 | ||
|
|
c6db763716 | ||
|
|
a3383af4ae | ||
|
|
6c56e44b61 | ||
|
|
64506a7080 | ||
|
|
fac9ae4b6c | ||
|
|
a2dc73afef | ||
|
|
59c5a34cd0 | ||
|
|
3321e6b0e2 | ||
|
|
8b7ac179fa | ||
|
|
ea745665c8 | ||
|
|
ca8f6ee10b | ||
|
|
3e51ff46f3 | ||
|
|
fa2e814559 | ||
|
|
87e337cbeb | ||
|
|
abb39df547 | ||
|
|
43e15bf911 | ||
|
|
4d79d0af89 | ||
|
|
69100d7db5 | ||
|
|
a064a6cf9b | ||
|
|
7953a9a3ce | ||
|
|
be3c6f210d | ||
|
|
f7cbddab4b | ||
|
|
d423818764 | ||
|
|
519acd43aa | ||
|
|
2682a0d9e4 | ||
|
|
8629ae048c | ||
|
|
905d01e804 | ||
|
|
0588bbc41d | ||
|
|
b308b4c54f | ||
|
|
c2c73d5460 | ||
|
|
e01bf57874 | ||
|
|
7ced93225b | ||
|
|
b5e61864af | ||
|
|
1e5aaea8f4 | ||
|
|
ab3bebf06a | ||
|
|
4a294d6a77 | ||
|
|
e0fda1a0bc | ||
|
|
d17da80f19 | ||
|
|
2e7658f857 | ||
|
|
53d0b28c7c | ||
|
|
33ba4d3871 | ||
|
|
225db6397d | ||
|
|
73b5d1b3f1 | ||
|
|
8da2eac6d0 | ||
|
|
fbd62153ee | ||
|
|
9145fa1c48 | ||
|
|
caa0af1258 | ||
|
|
7a230ee5f5 | ||
|
|
f237fa98d2 | ||
|
|
be4ae605a9 | ||
|
|
9c2cbc0ecb | ||
|
|
fb3009fc66 | ||
|
|
04c32e28cd | ||
|
|
645576c949 | ||
|
|
775bee3546 | ||
|
|
88aea96034 | ||
|
|
5f474dfaf5 | ||
|
|
fe7aad0835 | ||
|
|
79746efa2d | ||
|
|
a158021f46 | ||
|
|
2d91a893f7 | ||
|
|
dd4561d985 | ||
|
|
92764eeae0 | ||
|
|
b72808ab40 | ||
|
|
14f3f99218 | ||
|
|
d7130d9b67 | ||
|
|
3f94eee4d5 | ||
|
|
72cbdcbc8d | ||
|
|
e33b49e78c | ||
|
|
8e04945d4e | ||
|
|
3ca5da55cb | ||
|
|
ea30373a09 | ||
|
|
4b4757d0e5 | ||
|
|
4bc837509d | ||
|
|
c9d1e8dc65 | ||
|
|
88b8a192b5 | ||
|
|
94fbf627ba | ||
|
|
45fbdb8411 | ||
|
|
d9c947ccd0 | ||
|
|
2b670a5ae1 | ||
|
|
1af0178b50 | ||
|
|
3ec5d894b3 | ||
|
|
d81585ccc3 | ||
|
|
38f91bce1c | ||
|
|
2d41dd6ae0 | ||
|
|
1705a21f68 | ||
|
|
164d79898a | ||
|
|
50f809d290 | ||
|
|
39284b475d | ||
|
|
d44950d46c | ||
|
|
e9b55bc207 | ||
|
|
5470f08fee | ||
|
|
f9a3bbd7fa | ||
|
|
9d3165dc65 | ||
|
|
3475d39f37 | ||
|
|
44782b1ddf | ||
|
|
dd8d5fd84c | ||
|
|
e8f2d9d0dd | ||
|
|
a2de3b5d80 | ||
|
|
a2960c45bc | ||
|
|
dc91624597 | ||
|
|
223ec180fc | ||
|
|
0116572fec | ||
|
|
5350e5385c | ||
|
|
8f18c4fd45 | ||
|
|
8538fbabe5 | ||
|
|
9367b34bbe | ||
|
|
a766044cb4 | ||
|
|
0eb385e49f | ||
|
|
e30136dace | ||
|
|
c50dee479a | ||
|
|
58ef292fa7 | ||
|
|
61b728fea7 | ||
|
|
b782eeb839 | ||
|
|
77314d4b8d | ||
|
|
c79d1d24b3 | ||
|
|
5dbea8ca09 | ||
|
|
09a1c17fb4 | ||
|
|
a0632bcac2 | ||
|
|
07d57ebe8c | ||
|
|
325c88018c | ||
|
|
dcb1102746 | ||
|
|
636d3c02c4 | ||
|
|
5b119ded17 | ||
|
|
f25ae870c5 | ||
|
|
49673262e4 | ||
|
|
3ed814c1f7 | ||
|
|
8b858e5407 | ||
|
|
30145894b6 | ||
|
|
8df4c27203 | ||
|
|
28166f79a1 | ||
|
|
2fe67137c5 | ||
|
|
a6ea2de691 | ||
|
|
7ffcde002e | ||
|
|
a3f66b8ab1 | ||
|
|
43cee53dc8 |
111
.editorconfig
@@ -1,3 +1,112 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Don't use tabs for indentation.
|
||||
[*]
|
||||
indent_style = space
|
||||
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
charset = utf-8-bom
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Prefix private members with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_members
|
||||
dotnet_naming_rule.private_members_with_underscore.style = underscore_prefix
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_members.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_members.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_members.required_modifiers = readonly
|
||||
|
||||
dotnet_naming_style.underscore_prefix.capitalization = camel_case
|
||||
dotnet_naming_style.underscore_prefix.required_prefix = _
|
||||
dotnet_naming_style.underscore_prefix.required_suffix =
|
||||
dotnet_naming_style.underscore_prefix.word_separator =
|
||||
|
||||
# Async methods should have "Async" suffix
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.any_async_methods.required_modifiers = async
|
||||
|
||||
dotnet_naming_style.end_in_async.required_prefix =
|
||||
dotnet_naming_style.end_in_async.required_suffix = Async
|
||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||
dotnet_naming_style.end_in_async.word_separator =
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a expression-body
|
||||
csharp_style_expression_bodied_methods = true:none
|
||||
csharp_style_expression_bodied_constructors = true:none
|
||||
csharp_style_expression_bodied_operators = true:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# All files
|
||||
[*]
|
||||
guidelines = 120
|
||||
guidelines = 120
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
<!--
|
||||
<!-- Comment:
|
||||
Please do not submit feature requests. The [Community Forums][1] has a
|
||||
section for submitting, voting for, and discussing product feature requests.
|
||||
[1]: https://community.bitwarden.com
|
||||
-->
|
||||
|
||||
## Describe the Bug
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
## Steps To Reproduce
|
||||
|
||||
<!-- Comment:
|
||||
How can we reproduce the behavior:
|
||||
-->
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. Click on '...'
|
||||
|
||||
## Expected Result
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
## Actual Result
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what is happening.
|
||||
-->
|
||||
|
||||
## Screenshots or Videos
|
||||
|
||||
<!-- Comment:
|
||||
If applicable, add screenshots and/or a short video to help explain your problem.
|
||||
-->
|
||||
|
||||
## Environment
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- Operating system: [e.g. iOS 8.1]
|
||||
- Build Version (go to "Settings" → "About" in the app): [e.g. 2.3.0 (2221)]
|
||||
- Is this a Beta release? [Y/N]
|
||||
|
||||
## Additional Context
|
||||
|
||||
<!-- Comment:
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin iOS, and Xamarin Forms.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-android.png" alt="" width="300" height="533" /> <img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-ios.png" alt="" width="300" height="533" />
|
||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-android-myvault.png" alt="" width="300" height="533" /> <img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-ios-myvault.png" alt="" width="300" height="533" />
|
||||
|
||||
# Build/Run
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
- Visual Studio 2019 Preview
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
@@ -71,7 +71,7 @@ build_script:
|
||||
mkdir dist
|
||||
cp CNAME ./dist
|
||||
cd store
|
||||
chmod 600 fdroid/config.py keystore.jks
|
||||
chmod 600 fdroid/config.py fdroid/keystore.jks
|
||||
mkdir -p temp/fdroid
|
||||
TEMP_DIR="$APPVEYOR_BUILD_FOLDER/store/temp/fdroid"
|
||||
cd fdroid
|
||||
@@ -95,7 +95,9 @@ build_script:
|
||||
msbuild bitwarden-mobile.sln `
|
||||
"/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" `
|
||||
"/p:Configuration=Release"
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
.\src\Android\ci-build-apks.ps1
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden.apk
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden-fdroid.apk
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.28307.539
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29009.5
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}"
|
||||
EndProject
|
||||
@@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "google", "google", "{2E3996
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{76690DFB-B7F4-4781-83E4-113FDC450AFE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
appveyor.yml = appveyor.yml
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
@@ -35,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Core", "src\iOS.Core\iO
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS", "src\iOS\iOS.csproj", "{599E0201-420A-4C3E-A7BA-5349F72E0B15}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Extension", "src\iOS.Extension\iOS.Extension.csproj", "{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Autofill", "src\iOS.Autofill\iOS.Autofill.csproj", "{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
|
||||
@@ -99,24 +104,24 @@ Global
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
@@ -141,18 +146,18 @@ Global
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
@@ -231,18 +236,18 @@ Global
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhone.Build.0 = Release|Any CPU
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
@@ -286,6 +291,62 @@ Global
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|Any CPU.Build.0 = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhone.Build.0 = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|Any CPU.ActiveCfg = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhone.ActiveCfg = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhone.Build.0 = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|Any CPU.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|Any CPU.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhone.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhone.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|Any CPU.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhone.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|Any CPU.Build.0 = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhone.Build.0 = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|Any CPU.ActiveCfg = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhone.ActiveCfg = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhone.Build.0 = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|Any CPU.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|Any CPU.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhone.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhone.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|Any CPU.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -299,6 +360,8 @@ Global
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D} = {2E399654-26A2-46F6-B9CA-1B496A3F370A}
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7D436EA3-8B7E-45D2-8D14-0730BD2E0410}
|
||||
|
||||
8
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bitwarden-fdroid",
|
||||
"name": "bitwarden-mobile",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
@@ -196,9 +196,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android.Content;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Views;
|
||||
using Android.Views.Accessibility;
|
||||
using Android.Widget;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Plugin.CurrentActivity;
|
||||
|
||||
namespace Bit.Droid.Accessibility
|
||||
{
|
||||
@@ -32,14 +40,16 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("org.iron.srware", "url_bar"),
|
||||
new Browser("com.sec.android.app.sbrowser", "location_bar_edit_text"),
|
||||
new Browser("com.sec.android.app.sbrowser.beta", "location_bar_edit_text"),
|
||||
new Browser("com.yandex.browser", "bro_omnibar_address_title_text",
|
||||
new Browser("com.yandex.browser", "bro_omnibar_address_title_text,bro_omnibox_collapsed_title",
|
||||
(s) => s.Split(new char[]{' ', ' '}).FirstOrDefault()), // 0 = Regular Space, 1 = No-break space (00A0)
|
||||
new Browser("org.mozilla.firefox", "url_bar_title"),
|
||||
new Browser("org.mozilla.firefox_beta", "url_bar_title"),
|
||||
new Browser("org.mozilla.fennec_aurora", "url_bar_title"),
|
||||
new Browser("org.mozilla.fennec_fdroid", "url_bar_title"),
|
||||
new Browser("org.mozilla.focus", "display_url"),
|
||||
new Browser("org.mozilla.klar", "display_url"),
|
||||
new Browser("org.mozilla.fenix", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("org.mozilla.fenix.nightly", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("org.mozilla.reference.browser", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("com.ghostery.android.ghostery", "search_field"),
|
||||
new Browser("org.adblockplus.browser", "url_bar_title"),
|
||||
@@ -61,6 +71,11 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||
new Browser("com.ecosia.android", "url_bar"),
|
||||
new Browser("com.qwant.liberty", "url_bar_title"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_black", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_test", "url_text"),
|
||||
new Browser("com.vivaldi.browser", "url_bar"),
|
||||
new Browser("com.feedback.browser.wjbrowser", "addressbar_url"),
|
||||
}.ToDictionary(n => n.PackageName);
|
||||
|
||||
// Known packages to skip
|
||||
@@ -80,13 +95,18 @@ namespace Bit.Droid.Accessibility
|
||||
"com.teslacoilsw.launcher.prime",
|
||||
"is.shortcut",
|
||||
"me.craftsapp.nlauncher",
|
||||
"com.ss.squarehome2"
|
||||
"com.ss.squarehome2",
|
||||
"com.treydev.pns"
|
||||
};
|
||||
|
||||
public static void PrintTestData(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
|
||||
var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
|
||||
foreach(var node in testNodesData)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("Node: {0} = {1}", node.id, node.text);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetUri(AccessibilityNodeInfo root)
|
||||
@@ -95,12 +115,27 @@ namespace Bit.Droid.Accessibility
|
||||
if(SupportedBrowsers.ContainsKey(root.PackageName))
|
||||
{
|
||||
var browser = SupportedBrowsers[root.PackageName];
|
||||
var addressNode = root.FindAccessibilityNodeInfosByViewId(
|
||||
$"{root.PackageName}:id/{browser.UriViewId}").FirstOrDefault();
|
||||
AccessibilityNodeInfo addressNode = null;
|
||||
foreach(var uriViewId in browser.UriViewId.Split(","))
|
||||
{
|
||||
addressNode = root.FindAccessibilityNodeInfosByViewId(
|
||||
$"{root.PackageName}:id/{uriViewId}").FirstOrDefault();
|
||||
if(addressNode != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(addressNode != null)
|
||||
{
|
||||
uri = ExtractUri(uri, addressNode, browser);
|
||||
addressNode.Dispose();
|
||||
addressNode.Recycle();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return null to prevent overwriting notification pendingIntent uri with browser packageName
|
||||
// (we login to pages, not browsers)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
@@ -161,7 +196,6 @@ namespace Bit.Droid.Accessibility
|
||||
return n?.ClassName?.Contains("EditText") ?? false;
|
||||
}
|
||||
|
||||
|
||||
public static void FillCredentials(AccessibilityNodeInfo usernameNode,
|
||||
IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
||||
{
|
||||
@@ -192,7 +226,7 @@ namespace Bit.Droid.Accessibility
|
||||
nodes = new NodeList();
|
||||
}
|
||||
var dispose = disposeIfUnused;
|
||||
if(n != null && recursionDepth < 50)
|
||||
if(n != null && recursionDepth < 100)
|
||||
{
|
||||
var add = n.WindowId == e.WindowId &&
|
||||
!(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) &&
|
||||
@@ -206,7 +240,11 @@ namespace Bit.Droid.Accessibility
|
||||
for(var i = 0; i < n.ChildCount; i++)
|
||||
{
|
||||
var childNode = n.GetChild(i);
|
||||
if(i > 100)
|
||||
if(childNode == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if(i > 100)
|
||||
{
|
||||
Android.Util.Log.Info(BitwardenTag, "Too many child iterations.");
|
||||
break;
|
||||
@@ -223,6 +261,7 @@ namespace Bit.Droid.Accessibility
|
||||
}
|
||||
if(dispose)
|
||||
{
|
||||
n?.Recycle();
|
||||
n?.Dispose();
|
||||
}
|
||||
return nodes;
|
||||
@@ -232,15 +271,274 @@ namespace Bit.Droid.Accessibility
|
||||
IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
||||
{
|
||||
var allEditTexts = GetWindowNodes(root, e, n => EditText(n), false);
|
||||
var usernameEditText = GetUsernameEditText(allEditTexts);
|
||||
var usernameEditText = GetUsernameEditTextIfPasswordExists(allEditTexts);
|
||||
FillCredentials(usernameEditText, passwordNodes);
|
||||
allEditTexts.Dispose();
|
||||
usernameEditText = null;
|
||||
}
|
||||
|
||||
public static AccessibilityNodeInfo GetUsernameEditText(IEnumerable<AccessibilityNodeInfo> allEditTexts)
|
||||
public static AccessibilityNodeInfo GetUsernameEditTextIfPasswordExists(
|
||||
IEnumerable<AccessibilityNodeInfo> allEditTexts)
|
||||
{
|
||||
return allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
|
||||
AccessibilityNodeInfo previousEditText = null;
|
||||
foreach(var editText in allEditTexts)
|
||||
{
|
||||
if(editText.Password)
|
||||
{
|
||||
return previousEditText;
|
||||
}
|
||||
previousEditText = editText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsUsernameEditText(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
var allEditTexts = GetWindowNodes(root, e, n => EditText(n), false);
|
||||
var usernameEditText = GetUsernameEditTextIfPasswordExists(allEditTexts);
|
||||
|
||||
var isUsernameEditText = false;
|
||||
if(usernameEditText != null)
|
||||
{
|
||||
isUsernameEditText = IsSameNode(usernameEditText, e.Source);
|
||||
}
|
||||
allEditTexts.Dispose();
|
||||
|
||||
return isUsernameEditText;
|
||||
}
|
||||
|
||||
public static bool IsSameNode(AccessibilityNodeInfo node1, AccessibilityNodeInfo node2)
|
||||
{
|
||||
if(node1 != null && node2 != null)
|
||||
{
|
||||
return node1.Equals(node2) || node1.GetHashCode() == node2.GetHashCode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool OverlayPermitted()
|
||||
{
|
||||
if(Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||
{
|
||||
return Settings.CanDrawOverlays(Android.App.Application.Context);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO do older android versions require a check?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static LinearLayout GetOverlayView(Context context)
|
||||
{
|
||||
var inflater = (LayoutInflater)context.GetSystemService(Context.LayoutInflaterService);
|
||||
var view = (LinearLayout)inflater.Inflate(Resource.Layout.autofill_listitem, null);
|
||||
var text1 = (TextView)view.FindViewById(Resource.Id.text1);
|
||||
var text2 = (TextView)view.FindViewById(Resource.Id.text2);
|
||||
var icon = (ImageView)view.FindViewById(Resource.Id.icon);
|
||||
text1.Text = AppResources.AutofillWithBitwarden;
|
||||
text2.Text = AppResources.GoToMyVault;
|
||||
icon.SetImageResource(Resource.Drawable.icon);
|
||||
return view;
|
||||
}
|
||||
|
||||
public static WindowManagerLayoutParams GetOverlayLayoutParams()
|
||||
{
|
||||
WindowManagerTypes windowManagerType;
|
||||
if(Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
||||
{
|
||||
windowManagerType = WindowManagerTypes.ApplicationOverlay;
|
||||
}
|
||||
else
|
||||
{
|
||||
windowManagerType = WindowManagerTypes.Phone;
|
||||
}
|
||||
|
||||
var layoutParams = new WindowManagerLayoutParams(
|
||||
ViewGroup.LayoutParams.WrapContent,
|
||||
ViewGroup.LayoutParams.WrapContent,
|
||||
windowManagerType,
|
||||
WindowManagerFlags.NotFocusable | WindowManagerFlags.NotTouchModal,
|
||||
Format.Transparent);
|
||||
layoutParams.Gravity = GravityFlags.Top | GravityFlags.Left;
|
||||
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo anchorView, int overlayViewHeight,
|
||||
bool isOverlayAboveAnchor)
|
||||
{
|
||||
var anchorViewRect = new Rect();
|
||||
anchorView.GetBoundsInScreen(anchorViewRect);
|
||||
var anchorViewX = anchorViewRect.Left;
|
||||
var anchorViewY = isOverlayAboveAnchor ? anchorViewRect.Top : anchorViewRect.Bottom;
|
||||
anchorViewRect.Dispose();
|
||||
|
||||
if(isOverlayAboveAnchor)
|
||||
{
|
||||
anchorViewY -= overlayViewHeight;
|
||||
}
|
||||
anchorViewY -= GetStatusBarHeight();
|
||||
|
||||
return new Point(anchorViewX, anchorViewY);
|
||||
}
|
||||
|
||||
public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo anchorNode, AccessibilityNodeInfo root,
|
||||
IEnumerable<AccessibilityWindowInfo> windows, int overlayViewHeight, bool isOverlayAboveAnchor)
|
||||
{
|
||||
Point point = null;
|
||||
if(anchorNode != null)
|
||||
{
|
||||
// Update node's info since this is still a reference from an older event
|
||||
anchorNode.Refresh();
|
||||
if(!anchorNode.VisibleToUser)
|
||||
{
|
||||
return new Point(-1, -1);
|
||||
}
|
||||
if(!anchorNode.Focused)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// node.VisibleToUser doesn't always give us exactly what we want, so attempt to tighten up the range
|
||||
// of visibility
|
||||
var minY = 0;
|
||||
int maxY;
|
||||
if(windows != null)
|
||||
{
|
||||
if(IsStatusBarExpanded(windows))
|
||||
{
|
||||
return new Point(-1, -1);
|
||||
}
|
||||
maxY = GetApplicationVisibleHeight(windows);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootNodeHeight = GetNodeHeight(root);
|
||||
if(rootNodeHeight == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
maxY = rootNodeHeight - GetNavigationBarHeight();
|
||||
}
|
||||
|
||||
point = GetOverlayAnchorPosition(anchorNode, overlayViewHeight, isOverlayAboveAnchor);
|
||||
if(point.Y < minY)
|
||||
{
|
||||
if(isOverlayAboveAnchor)
|
||||
{
|
||||
// view nearing bounds, anchor to bottom
|
||||
point.X = -1;
|
||||
point.Y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// view out of bounds, hide overlay
|
||||
point.X = -1;
|
||||
point.Y = -1;
|
||||
}
|
||||
}
|
||||
else if(point.Y > maxY)
|
||||
{
|
||||
if(isOverlayAboveAnchor)
|
||||
{
|
||||
// view out of bounds, hide overlay
|
||||
point.X = -1;
|
||||
point.Y = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// view nearing bounds, anchor to top
|
||||
point.X = 0;
|
||||
point.Y = -1;
|
||||
}
|
||||
}
|
||||
else if(isOverlayAboveAnchor && point.Y < (maxY - overlayViewHeight - GetNodeHeight(anchorNode)))
|
||||
{
|
||||
// This else block forces the overlay to return to bottom alignment as soon as space is available
|
||||
// below the anchor view. Removing this will change the behavior to wait until there isn't enough
|
||||
// space above the anchor view before returning to bottom alignment.
|
||||
point.X = -1;
|
||||
point.Y = 0;
|
||||
}
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
public static bool IsStatusBarExpanded(IEnumerable<AccessibilityWindowInfo> windows)
|
||||
{
|
||||
if(windows != null && windows.Any())
|
||||
{
|
||||
var isSystemWindowsOnly = true;
|
||||
foreach(var window in windows)
|
||||
{
|
||||
if(window.Type != AccessibilityWindowType.System)
|
||||
{
|
||||
isSystemWindowsOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isSystemWindowsOnly;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int GetApplicationVisibleHeight(IEnumerable<AccessibilityWindowInfo> windows)
|
||||
{
|
||||
var appWindowHeight = 0;
|
||||
var nonAppWindowHeight = 0;
|
||||
if(windows != null)
|
||||
{
|
||||
foreach(var window in windows)
|
||||
{
|
||||
var windowRect = new Rect();
|
||||
window.GetBoundsInScreen(windowRect);
|
||||
if(window.Type == AccessibilityWindowType.Application)
|
||||
{
|
||||
appWindowHeight += windowRect.Height();
|
||||
}
|
||||
else
|
||||
{
|
||||
nonAppWindowHeight += windowRect.Height();
|
||||
}
|
||||
}
|
||||
}
|
||||
return appWindowHeight - nonAppWindowHeight;
|
||||
}
|
||||
|
||||
public static int GetNodeHeight(AccessibilityNodeInfo node)
|
||||
{
|
||||
if(node == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var nodeRect = new Rect();
|
||||
node.GetBoundsInScreen(nodeRect);
|
||||
var nodeRectHeight = nodeRect.Height();
|
||||
nodeRect.Dispose();
|
||||
return nodeRectHeight;
|
||||
}
|
||||
|
||||
private static int GetStatusBarHeight()
|
||||
{
|
||||
return GetSystemResourceDimenPx("status_bar_height");
|
||||
}
|
||||
|
||||
private static int GetNavigationBarHeight()
|
||||
{
|
||||
return GetSystemResourceDimenPx("navigation_bar_height");
|
||||
}
|
||||
|
||||
private static int GetSystemResourceDimenPx(string resName)
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var barHeight = 0;
|
||||
var resourceId = activity.Resources.GetIdentifier(resName, "dimen", "android");
|
||||
if(resourceId > 0)
|
||||
{
|
||||
barHeight = activity.Resources.GetDimensionPixelSize(resourceId);
|
||||
}
|
||||
return barHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Views.Accessibility;
|
||||
using Android.Widget;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Java.Util;
|
||||
|
||||
namespace Bit.Droid.Accessibility
|
||||
{
|
||||
@@ -20,19 +25,25 @@ namespace Bit.Droid.Accessibility
|
||||
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
||||
public class AccessibilityService : Android.AccessibilityServices.AccessibilityService
|
||||
{
|
||||
private NotificationChannel _notificationChannel;
|
||||
|
||||
private const int AutoFillNotificationId = 34573;
|
||||
private const string BitwardenPackage = "com.x8bit.bitwarden";
|
||||
private const string BitwardenWebsite = "vault.bitwarden.com";
|
||||
|
||||
private IStorageService _storageService;
|
||||
private bool _settingAutofillPasswordField;
|
||||
private bool _settingAutofillPersistNotification;
|
||||
private DateTime? _lastSettingsReload = null;
|
||||
private TimeSpan _settingsReloadSpan = TimeSpan.FromMinutes(1);
|
||||
private long _lastNotificationTime = 0;
|
||||
private string _lastNotificationUri = null;
|
||||
private HashSet<string> _blacklistedUris;
|
||||
private AccessibilityNodeInfo _anchorNode = null;
|
||||
private int _lastAnchorX = 0;
|
||||
private int _lastAnchorY = 0;
|
||||
private bool _isOverlayAboveAnchor = false;
|
||||
private static bool _overlayAnchorObserverRunning = false;
|
||||
private IWindowManager _windowManager = null;
|
||||
private LinearLayout _overlayView = null;
|
||||
private int _overlayViewHeight = 0;
|
||||
private long _lastAutoFillTime = 0;
|
||||
private Java.Lang.Runnable _overlayAnchorObserverRunnable = null;
|
||||
private Handler _handler = new Handler(Looper.MainLooper);
|
||||
|
||||
private HashSet<string> _launcherPackageNames = null;
|
||||
private DateTime? _lastLauncherSetBuilt = null;
|
||||
private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1);
|
||||
@@ -53,116 +64,80 @@ namespace Bit.Droid.Accessibility
|
||||
|
||||
if(SkipPackage(e?.PackageName))
|
||||
{
|
||||
if(e?.PackageName != "com.android.systemui")
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// AccessibilityHelpers.PrintTestData(RootInActiveWindow, e);
|
||||
|
||||
// AccessibilityHelpers.PrintTestData(root, e);
|
||||
LoadServices();
|
||||
var settingsTask = LoadSettingsAsync();
|
||||
|
||||
var notificationManager = GetSystemService(NotificationService) as NotificationManager;
|
||||
var cancelNotification = true;
|
||||
AccessibilityNodeInfo root = null;
|
||||
|
||||
switch(e.EventType)
|
||||
{
|
||||
case EventTypes.ViewFocused:
|
||||
if(e.Source == null || !e.Source.Password || !_settingAutofillPasswordField)
|
||||
case EventTypes.ViewClicked:
|
||||
if(e.Source == null || e.PackageName == BitwardenPackage)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
break;
|
||||
}
|
||||
|
||||
root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if(e.PackageName == BitwardenPackage)
|
||||
|
||||
if(!(e.Source?.Password ?? false) && !AccessibilityHelpers.IsUsernameEditText(root, e))
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
CancelOverlayPrompt();
|
||||
break;
|
||||
}
|
||||
if(ScanAndAutofill(root, e, notificationManager, cancelNotification))
|
||||
if(ScanAndAutofill(root, e))
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
else
|
||||
{
|
||||
OverlayPromptToAutofill(root, e);
|
||||
}
|
||||
break;
|
||||
case EventTypes.WindowContentChanged:
|
||||
case EventTypes.WindowStateChanged:
|
||||
if(_settingAutofillPasswordField && e.Source.Password)
|
||||
if(AccessibilityHelpers.LastCredentials == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if(_settingAutofillPasswordField && AccessibilityHelpers.LastCredentials == null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_lastNotificationUri))
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
break;
|
||||
}
|
||||
var uri = AccessibilityHelpers.GetUri(root);
|
||||
if(uri != _lastNotificationUri)
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
}
|
||||
else if(uri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
CancelNotification(notificationManager, 30000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(e.PackageName == BitwardenPackage)
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
CancelOverlayPrompt();
|
||||
break;
|
||||
}
|
||||
|
||||
if(_settingAutofillPersistNotification)
|
||||
root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
var uri = AccessibilityHelpers.GetUri(root);
|
||||
if(uri != null && !uri.Contains(BitwardenWebsite))
|
||||
{
|
||||
var needToFill = AccessibilityHelpers.NeedToAutofill(
|
||||
AccessibilityHelpers.LastCredentials, uri);
|
||||
if(needToFill)
|
||||
{
|
||||
var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e,
|
||||
n => n.Password, false);
|
||||
needToFill = passwordNodes.Any();
|
||||
if(needToFill)
|
||||
{
|
||||
AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes);
|
||||
}
|
||||
passwordNodes.Dispose();
|
||||
}
|
||||
if(!needToFill)
|
||||
{
|
||||
NotifyToAutofill(uri, notificationManager);
|
||||
cancelNotification = false;
|
||||
}
|
||||
}
|
||||
AccessibilityHelpers.LastCredentials = null;
|
||||
break;
|
||||
}
|
||||
else
|
||||
if(ScanAndAutofill(root, e))
|
||||
{
|
||||
cancelNotification = ScanAndAutofill(root, e, notificationManager, cancelNotification);
|
||||
}
|
||||
|
||||
if(cancelNotification)
|
||||
{
|
||||
CancelNotification(notificationManager);
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
notificationManager?.Dispose();
|
||||
root.Dispose();
|
||||
e.Dispose();
|
||||
}
|
||||
// Suppress exceptions so that service doesn't crash.
|
||||
catch { }
|
||||
catch(Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInterrupt()
|
||||
@@ -170,9 +145,9 @@ namespace Bit.Droid.Accessibility
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
public bool ScanAndAutofill(AccessibilityNodeInfo root, AccessibilityEvent e,
|
||||
NotificationManager notificationManager, bool cancelNotification)
|
||||
public bool ScanAndAutofill(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
var filled = false;
|
||||
var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e, n => n.Password, false);
|
||||
if(passwordNodes.Count > 0)
|
||||
{
|
||||
@@ -182,11 +157,8 @@ namespace Bit.Droid.Accessibility
|
||||
if(AccessibilityHelpers.NeedToAutofill(AccessibilityHelpers.LastCredentials, uri))
|
||||
{
|
||||
AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifyToAutofill(uri, notificationManager);
|
||||
cancelNotification = false;
|
||||
filled = true;
|
||||
_lastAutoFillTime = Java.Lang.JavaSystem.CurrentTimeMillis();
|
||||
}
|
||||
}
|
||||
AccessibilityHelpers.LastCredentials = null;
|
||||
@@ -200,69 +172,197 @@ namespace Bit.Droid.Accessibility
|
||||
});
|
||||
}
|
||||
passwordNodes.Dispose();
|
||||
return cancelNotification;
|
||||
return filled;
|
||||
}
|
||||
|
||||
public void CancelNotification(NotificationManager notificationManager, long limit = 250)
|
||||
private void CancelOverlayPrompt()
|
||||
{
|
||||
if(Java.Lang.JavaSystem.CurrentTimeMillis() - _lastNotificationTime < limit)
|
||||
_overlayAnchorObserverRunning = false;
|
||||
|
||||
if(_windowManager != null && _overlayView != null)
|
||||
{
|
||||
_windowManager.RemoveViewImmediate(_overlayView);
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed");
|
||||
}
|
||||
|
||||
_overlayView = null;
|
||||
_lastAnchorX = 0;
|
||||
_lastAnchorY = 0;
|
||||
_isOverlayAboveAnchor = false;
|
||||
|
||||
if(_anchorNode != null)
|
||||
{
|
||||
_anchorNode.Recycle();
|
||||
_anchorNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverlayPromptToAutofill(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
if(!AccessibilityHelpers.OverlayPermitted())
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> Overlay Permission not granted");
|
||||
Toast.MakeText(this, AppResources.AccessibilityOverlayPermissionAlert, ToastLength.Long).Show();
|
||||
return;
|
||||
}
|
||||
_lastNotificationUri = null;
|
||||
notificationManager?.Cancel(AutoFillNotificationId);
|
||||
}
|
||||
|
||||
private void NotifyToAutofill(string uri, NotificationManager notificationManager)
|
||||
{
|
||||
if(notificationManager == null || string.IsNullOrWhiteSpace(uri))
|
||||
if(_overlayView != null || _anchorNode != null || _overlayAnchorObserverRunning)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
|
||||
if(Java.Lang.JavaSystem.CurrentTimeMillis() - _lastAutoFillTime < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = AccessibilityHelpers.GetUri(root);
|
||||
var fillable = !string.IsNullOrWhiteSpace(uri);
|
||||
if(fillable)
|
||||
{
|
||||
if(_blacklistedUris != null && _blacklistedUris.Any())
|
||||
{
|
||||
if(Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri) && parsedUri.Scheme.StartsWith("http"))
|
||||
{
|
||||
fillable = !_blacklistedUris.Contains(
|
||||
string.Format("{0}://{1}", parsedUri.Scheme, parsedUri.Host));
|
||||
}
|
||||
else
|
||||
{
|
||||
fillable = !_blacklistedUris.Contains(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!fillable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Java.Lang.JavaSystem.CurrentTimeMillis();
|
||||
var intent = new Intent(this, typeof(AccessibilityActivity));
|
||||
intent.PutExtra("uri", uri);
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.UpdateCurrent);
|
||||
|
||||
var notificationContent = Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch ?
|
||||
AppResources.BitwardenAutofillServiceNotificationContent :
|
||||
AppResources.BitwardenAutofillServiceNotificationContentOld;
|
||||
|
||||
var builder = new Notification.Builder(this);
|
||||
builder.SetSmallIcon(Resource.Drawable.shield)
|
||||
.SetContentTitle(AppResources.BitwardenAutofillService)
|
||||
.SetContentText(notificationContent)
|
||||
.SetTicker(notificationContent)
|
||||
.SetWhen(now)
|
||||
.SetContentIntent(pendingIntent);
|
||||
|
||||
if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch)
|
||||
_overlayView = AccessibilityHelpers.GetOverlayView(this);
|
||||
_overlayView.Measure(View.MeasureSpec.MakeMeasureSpec(0, 0),
|
||||
View.MeasureSpec.MakeMeasureSpec(0, 0));
|
||||
_overlayViewHeight = _overlayView.MeasuredHeight;
|
||||
_overlayView.Click += (sender, eventArgs) =>
|
||||
{
|
||||
builder.SetVisibility(NotificationVisibility.Secret)
|
||||
.SetColor(Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
|
||||
Resource.Color.primary));
|
||||
CancelOverlayPrompt();
|
||||
StartActivity(intent);
|
||||
};
|
||||
|
||||
var layoutParams = AccessibilityHelpers.GetOverlayLayoutParams();
|
||||
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(e.Source, _overlayViewHeight,
|
||||
_isOverlayAboveAnchor);
|
||||
layoutParams.X = anchorPosition.X;
|
||||
layoutParams.Y = anchorPosition.Y;
|
||||
|
||||
if(_windowManager == null)
|
||||
{
|
||||
_windowManager = GetSystemService(WindowService).JavaCast<IWindowManager>();
|
||||
}
|
||||
if(Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
||||
|
||||
_anchorNode = e.Source;
|
||||
_lastAnchorX = anchorPosition.X;
|
||||
_lastAnchorY = anchorPosition.Y;
|
||||
|
||||
_windowManager.AddView(_overlayView, layoutParams);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Added at X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
|
||||
StartOverlayAnchorObserver();
|
||||
}
|
||||
|
||||
private void StartOverlayAnchorObserver()
|
||||
{
|
||||
if(_overlayAnchorObserverRunning)
|
||||
{
|
||||
if(_notificationChannel == null)
|
||||
return;
|
||||
}
|
||||
|
||||
_overlayAnchorObserverRunning = true;
|
||||
_overlayAnchorObserverRunnable = new Java.Lang.Runnable(() =>
|
||||
{
|
||||
if(_overlayAnchorObserverRunning)
|
||||
{
|
||||
_notificationChannel = new NotificationChannel("bitwarden_autofill_service",
|
||||
AppResources.AutofillService, NotificationImportance.Low);
|
||||
notificationManager.CreateNotificationChannel(_notificationChannel);
|
||||
AdjustOverlayForScroll();
|
||||
_handler.PostDelayed(_overlayAnchorObserverRunnable, 250);
|
||||
}
|
||||
builder.SetChannelId(_notificationChannel.Id);
|
||||
}
|
||||
if(/*Build.VERSION.SdkInt <= BuildVersionCodes.N && */_settingAutofillPersistNotification)
|
||||
});
|
||||
|
||||
_handler.PostDelayed(_overlayAnchorObserverRunnable, 250);
|
||||
}
|
||||
|
||||
private void AdjustOverlayForScroll()
|
||||
{
|
||||
if(_overlayView == null || _anchorNode == null)
|
||||
{
|
||||
builder.SetPriority(-2);
|
||||
CancelOverlayPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastNotificationTime = now;
|
||||
_lastNotificationUri = uri;
|
||||
notificationManager.Notify(AutoFillNotificationId, builder.Build());
|
||||
builder.Dispose();
|
||||
var root = RootInActiveWindow;
|
||||
IEnumerable<AccessibilityWindowInfo> windows = null;
|
||||
if(Build.VERSION.SdkInt > BuildVersionCodes.Kitkat)
|
||||
{
|
||||
windows = Windows;
|
||||
}
|
||||
|
||||
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(_anchorNode, root, windows,
|
||||
_overlayViewHeight, _isOverlayAboveAnchor);
|
||||
if(anchorPosition == null)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
return;
|
||||
}
|
||||
else if(anchorPosition.X == -1 && anchorPosition.Y == -1)
|
||||
{
|
||||
if(_overlayView.Visibility != ViewStates.Gone)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Gone;
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Hidden");
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if(anchorPosition.X == -1)
|
||||
{
|
||||
_isOverlayAboveAnchor = false;
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Below Anchor");
|
||||
return;
|
||||
}
|
||||
else if(anchorPosition.Y == -1)
|
||||
{
|
||||
_isOverlayAboveAnchor = true;
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Above Anchor");
|
||||
return;
|
||||
}
|
||||
else if(anchorPosition.X == _lastAnchorX && anchorPosition.Y == _lastAnchorY)
|
||||
{
|
||||
if(_overlayView.Visibility != ViewStates.Visible)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Visible;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var layoutParams = AccessibilityHelpers.GetOverlayLayoutParams();
|
||||
layoutParams.X = anchorPosition.X;
|
||||
layoutParams.Y = anchorPosition.Y;
|
||||
|
||||
_lastAnchorX = anchorPosition.X;
|
||||
_lastAnchorY = anchorPosition.Y;
|
||||
|
||||
_windowManager.UpdateViewLayout(_overlayView, layoutParams);
|
||||
|
||||
if(_overlayView.Visibility != ViewStates.Visible)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Visible;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Updated to X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
}
|
||||
|
||||
private bool SkipPackage(string eventPackageName)
|
||||
@@ -300,10 +400,11 @@ namespace Bit.Droid.Accessibility
|
||||
if(_lastSettingsReload == null || (now - _lastSettingsReload.Value) > _settingsReloadSpan)
|
||||
{
|
||||
_lastSettingsReload = now;
|
||||
_settingAutofillPasswordField = await _storageService.GetAsync<bool>(
|
||||
Constants.AccessibilityAutofillPasswordFieldKey);
|
||||
_settingAutofillPersistNotification = await _storageService.GetAsync<bool>(
|
||||
Constants.AccessibilityAutofillPersistNotificationKey);
|
||||
var uris = await _storageService.GetAsync<List<string>>(Constants.AutofillBlacklistedUrisKey);
|
||||
if(uris != null)
|
||||
{
|
||||
_blacklistedUris = new HashSet<string>(uris);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ namespace Bit.Droid.Accessibility
|
||||
{
|
||||
foreach(var item in this)
|
||||
{
|
||||
item.Recycle();
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
|
||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
@@ -33,10 +33,10 @@
|
||||
<AndroidEnableMultiDex>true</AndroidEnableMultiDex>
|
||||
<AndroidSupportedAbis />
|
||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;Xamarin.Android.Net</AndroidLinkSkip>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;BitwardenCore;Xamarin.Android.Net;Xamarin.Android.Net.OldAndroidSSLSocketFactory</AndroidLinkSkip>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release</OutputPath>
|
||||
@@ -44,13 +44,15 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AndroidManagedSymbols>true</AndroidManagedSymbols>
|
||||
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
|
||||
<AndroidSupportedAbis />
|
||||
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
|
||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||
<AndroidEnableMultiDex>true</AndroidEnableMultiDex>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;Xamarin.Android.Net</AndroidLinkSkip>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;BitwardenCore;Xamarin.Android.Net;Xamarin.Android.Net.OldAndroidSSLSocketFactory</AndroidLinkSkip>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
<AndroidLinkMode>Full</AndroidLinkMode>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'FDroid|AnyCPU'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<OutputPath>bin\FDroid\</OutputPath>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
@@ -59,12 +61,13 @@
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<DefineConstants>FDROID</DefineConstants>
|
||||
<AndroidSupportedAbis />
|
||||
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
|
||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||
<AndroidEnableMultiDex>true</AndroidEnableMultiDex>
|
||||
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;Xamarin.Android.Net</AndroidLinkSkip>
|
||||
<AndroidLinkSkip>Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;BitwardenCore;Xamarin.Android.Net;Xamarin.Android.Net.OldAndroidSSLSocketFactory</AndroidLinkSkip>
|
||||
<AndroidLinkMode>Full</AndroidLinkMode>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Mono.Android" />
|
||||
@@ -79,19 +82,19 @@
|
||||
<Version>2.1.0.4</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Portable.BouncyCastle">
|
||||
<Version>1.8.5</Version>
|
||||
<Version>1.8.5.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.1.0</Version>
|
||||
<Version>1.3.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>60.1142.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Android.Support.Design" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v4" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.Design" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v4" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>60.1142.1</Version>
|
||||
</PackageReference>
|
||||
@@ -113,10 +116,11 @@
|
||||
<Compile Include="Effects\FixedSizeEffect.cs" />
|
||||
<Compile Include="Effects\SelectableLabelEffect.cs" />
|
||||
<Compile Include="Effects\TabBarEffect.cs" />
|
||||
<Compile Include="Migration\AndroidKeyStoreStorageService.cs" />
|
||||
<Compile Include="Push\FirebaseInstanceIdService.cs" />
|
||||
<Compile Include="Push\FirebaseMessagingService.cs" />
|
||||
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\RestrictionsChangedReceiver.cs" />
|
||||
<Compile Include="Receivers\EventUploadReceiver.cs" />
|
||||
<Compile Include="Receivers\LockAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\PackageReplacedReceiver.cs" />
|
||||
<Compile Include="Renderers\CipherViewCellRenderer.cs" />
|
||||
@@ -141,6 +145,7 @@
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
<Compile Include="Utilities\CustomFingerprintDialogFragment.cs" />
|
||||
<Compile Include="Utilities\HockeyAppCrashManagerListener.cs" />
|
||||
<Compile Include="Utilities\StaticStore.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\FontAwesome.ttf" />
|
||||
@@ -303,12 +308,6 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\autofill_use.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\accessibility_notification.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\accessibility_notification_icon.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\accessibility_step1.png" />
|
||||
</ItemGroup>
|
||||
@@ -321,12 +320,6 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\accessibility_notification.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\accessibility_notification_icon.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\accessibility_step1.png" />
|
||||
</ItemGroup>
|
||||
@@ -348,12 +341,6 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\accessibility_notification.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\accessibility_notification_icon.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\accessibility_step1.png" />
|
||||
</ItemGroup>
|
||||
@@ -549,5 +536,29 @@
|
||||
<SubType>Designer</SubType>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\accessibility_overlay.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\accessibility_overlay.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\accessibility_overlay.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\accessibility_permission.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\accessibility_permission.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\accessibility_permission.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\xml\app_restrictions.xml">
|
||||
<Generator>MSBuild:UpdateGeneratedFiles</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -53,20 +53,24 @@ namespace Bit.Droid.Autofill
|
||||
"com.ecosia.android",
|
||||
"com.opera.mini.native.beta",
|
||||
"org.mozilla.fennec_aurora",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"com.qwant.liberty",
|
||||
"com.opera.touch",
|
||||
"org.mozilla.fenix",
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"com.vivaldi.browser",
|
||||
};
|
||||
|
||||
// The URLs are blacklisted from autofilling
|
||||
public static HashSet<string> BlacklistedUris = new HashSet<string>
|
||||
{
|
||||
"androidapp://android",
|
||||
"androidapp://com.android.settings",
|
||||
"androidapp://com.x8bit.bitwarden",
|
||||
"androidapp://com.oneplus.applocker",
|
||||
"androidapp://com.android.settings",
|
||||
};
|
||||
|
||||
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core;
|
||||
using Android.Content;
|
||||
using Bit.Core.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
@@ -17,7 +18,7 @@ namespace Bit.Droid.Autofill
|
||||
private readonly AssistStructure _structure;
|
||||
private string _uri;
|
||||
private string _packageName;
|
||||
private string _webDomain;
|
||||
private string _website;
|
||||
|
||||
public Parser(AssistStructure structure, Context applicationContext)
|
||||
{
|
||||
@@ -36,14 +37,14 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
return _uri;
|
||||
}
|
||||
var webDomainNull = string.IsNullOrWhiteSpace(WebDomain);
|
||||
if(webDomainNull && string.IsNullOrWhiteSpace(PackageName))
|
||||
var websiteNull = string.IsNullOrWhiteSpace(Website);
|
||||
if(websiteNull && string.IsNullOrWhiteSpace(PackageName))
|
||||
{
|
||||
_uri = null;
|
||||
}
|
||||
else if(!webDomainNull)
|
||||
else if(!websiteNull)
|
||||
{
|
||||
_uri = string.Concat("http://", WebDomain);
|
||||
_uri = Website;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -66,16 +67,16 @@ namespace Bit.Droid.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
public string WebDomain
|
||||
public string Website
|
||||
{
|
||||
get => _webDomain;
|
||||
get => _website;
|
||||
set
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
_webDomain = _uri = null;
|
||||
_website = _uri = null;
|
||||
}
|
||||
_webDomain = value;
|
||||
_website = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,15 +97,24 @@ namespace Bit.Droid.Autofill
|
||||
|
||||
public void Parse()
|
||||
{
|
||||
string titlePackageId = null;
|
||||
for(var i = 0; i < _structure.WindowNodeCount; i++)
|
||||
{
|
||||
var node = _structure.GetWindowNodeAt(i);
|
||||
if(i == 0)
|
||||
{
|
||||
titlePackageId = GetTitlePackageId(node);
|
||||
}
|
||||
ParseNode(node.RootViewNode);
|
||||
}
|
||||
if(string.IsNullOrWhiteSpace(PackageName) && string.IsNullOrWhiteSpace(Website))
|
||||
{
|
||||
PackageName = titlePackageId;
|
||||
}
|
||||
if(!AutofillHelpers.TrustedBrowsers.Contains(PackageName) &&
|
||||
!AutofillHelpers.CompatBrowsers.Contains(PackageName))
|
||||
{
|
||||
WebDomain = null;
|
||||
Website = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +145,32 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
PackageName = node.IdPackage;
|
||||
}
|
||||
if(string.IsNullOrWhiteSpace(WebDomain) && !string.IsNullOrWhiteSpace(node.WebDomain))
|
||||
if(string.IsNullOrWhiteSpace(Website) && !string.IsNullOrWhiteSpace(node.WebDomain))
|
||||
{
|
||||
WebDomain = node.WebDomain;
|
||||
var scheme = "http";
|
||||
if((int)Build.VERSION.SdkInt >= 28)
|
||||
{
|
||||
scheme = node.WebScheme;
|
||||
}
|
||||
Website = string.Format("{0}://{1}", scheme, node.WebDomain);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTitlePackageId(WindowNode node)
|
||||
{
|
||||
if(node != null && !string.IsNullOrWhiteSpace(node.Title))
|
||||
{
|
||||
var slashPosition = node.Title.IndexOf('/');
|
||||
if(slashPosition > -1)
|
||||
{
|
||||
var packageId = node.Title.Substring(0, slashPosition);
|
||||
if(packageId.Contains("."))
|
||||
{
|
||||
return packageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using Bit.Core.Enums;
|
||||
using Android.Nfc;
|
||||
using Bit.App.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Support.V4.Content;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
@@ -29,17 +30,16 @@ namespace Bit.Droid
|
||||
[Register("com.x8bit.bitwarden.MainActivity")]
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
{
|
||||
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
|
||||
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IMessagingService _messagingService;
|
||||
private IBroadcasterService _broadcasterService;
|
||||
private IUserService _userService;
|
||||
private IAppIdService _appIdService;
|
||||
private IStorageService _storageService;
|
||||
private IStateService _stateService;
|
||||
private IEventService _eventService;
|
||||
private PendingIntent _lockAlarmPendingIntent;
|
||||
private PendingIntent _clearClipboardPendingIntent;
|
||||
private PendingIntent _eventUploadPendingIntent;
|
||||
private AppOptions _appOptions;
|
||||
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
||||
private Java.Util.Regex.Pattern _otpPattern =
|
||||
@@ -47,6 +47,9 @@ namespace Bit.Droid
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
||||
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
var alarmIntent = new Intent(this, typeof(LockAlarmReceiver));
|
||||
_lockAlarmPendingIntent = PendingIntent.GetBroadcast(this, 0, alarmIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
@@ -63,12 +66,12 @@ namespace Bit.Droid
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
|
||||
TabLayoutResource = Resource.Layout.Tabbar;
|
||||
ToolbarResource = Resource.Layout.Toolbar;
|
||||
|
||||
UpdateTheme(ThemeManager.GetTheme());
|
||||
UpdateTheme(ThemeManager.GetTheme(true));
|
||||
base.OnCreate(savedInstanceState);
|
||||
if(!CoreHelpers.InDebugMode())
|
||||
{
|
||||
@@ -77,8 +80,7 @@ namespace Bit.Droid
|
||||
|
||||
#if !FDROID
|
||||
var hockeyAppListener = new HockeyAppCrashManagerListener(_appIdService, _userService);
|
||||
var hockeyAppTask = hockeyAppListener.InitAsync();
|
||||
HockeyApp.Android.CrashManager.Register(this, HockeyAppId, hockeyAppListener);
|
||||
var hockeyAppTask = hockeyAppListener.InitAsync(this);
|
||||
#endif
|
||||
|
||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||
@@ -90,10 +92,10 @@ namespace Bit.Droid
|
||||
{
|
||||
if(message.Command == "scheduleLockTimer")
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
var lockOptionMinutes = (int)message.Data;
|
||||
var lockOptionMs = lockOptionMinutes * 60000;
|
||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + lockOptionMs + 10;
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Set(AlarmType.RtcWakeup, triggerMs, _lockAlarmPendingIntent);
|
||||
}
|
||||
else if(message.Command == "cancelLockTimer")
|
||||
@@ -101,6 +103,14 @@ namespace Bit.Droid
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Cancel(_lockAlarmPendingIntent);
|
||||
}
|
||||
else if(message.Command == "startEventTimer")
|
||||
{
|
||||
StartEventAlarm();
|
||||
}
|
||||
else if(message.Command == "stopEventTimer")
|
||||
{
|
||||
var task = StopEventAlarmAsync();
|
||||
}
|
||||
else if(message.Command == "finishMainActivity")
|
||||
{
|
||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
|
||||
@@ -141,6 +151,7 @@ namespace Bit.Droid
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
var setRestrictions = AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this);
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent intent)
|
||||
@@ -190,7 +201,8 @@ namespace Bit.Droid
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
if(requestCode == Constants.SelectFileRequestCode && resultCode == Result.Ok)
|
||||
if(resultCode == Result.Ok &&
|
||||
(requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode))
|
||||
{
|
||||
Android.Net.Uri uri = null;
|
||||
string fileName = null;
|
||||
@@ -202,9 +214,8 @@ namespace Bit.Droid
|
||||
else
|
||||
{
|
||||
// camera
|
||||
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
uri = Android.Net.Uri.FromFile(file);
|
||||
var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
|
||||
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
|
||||
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
||||
}
|
||||
|
||||
@@ -212,6 +223,14 @@ namespace Bit.Droid
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(requestCode == Constants.SaveFileRequestCode)
|
||||
{
|
||||
_messagingService.Send("selectSaveFileResult",
|
||||
new Tuple<string, string>(uri.ToString(), fileName));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using(var stream = ContentResolver.OpenInputStream(uri))
|
||||
@@ -367,10 +386,23 @@ namespace Bit.Droid
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _stateService.SaveAsync(Constants.LastClipboardValueKey, data.Item1);
|
||||
StaticStore.LastClipboardValue = data.Item1;
|
||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs.Value;
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent);
|
||||
}
|
||||
|
||||
private void StartEventAlarm()
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.SetInexactRepeating(AlarmType.ElapsedRealtime, 120000, 300000, _eventUploadPendingIntent);
|
||||
}
|
||||
|
||||
private async Task StopEventAlarmAsync()
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Cancel(_eventUploadPendingIntent);
|
||||
await _eventService.UploadEventsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Services;
|
||||
@@ -13,7 +15,10 @@ using Bit.Droid.Services;
|
||||
using Bit.Droid.Utilities;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
using Plugin.Fingerprint.Abstractions;
|
||||
using Xamarin.Android.Net;
|
||||
#if !FDROID
|
||||
using Android.Gms.Security;
|
||||
#endif
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
@@ -23,7 +28,11 @@ namespace Bit.Droid
|
||||
[Application(Debuggable = false)]
|
||||
#endif
|
||||
[Register("com.x8bit.bitwarden.MainApplication")]
|
||||
#if FDROID
|
||||
public class MainApplication : Application
|
||||
#else
|
||||
public class MainApplication : Application, ProviderInstaller.IProviderInstallListener
|
||||
#endif
|
||||
{
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership transer)
|
||||
: base(handle, transer)
|
||||
@@ -31,13 +40,15 @@ namespace Bit.Droid
|
||||
if(ServiceContainer.RegisteredServices.Count == 0)
|
||||
{
|
||||
RegisterLocalServices();
|
||||
ServiceContainer.Init();
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
var task = App.Migration.MigrationHelpers.PerformMigrationAsync();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
ServiceContainer.Init(deviceActionService.DeviceUserAgent);
|
||||
}
|
||||
#if !FDROID
|
||||
if(Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
{
|
||||
ProviderInstaller.InstallIfNeededAsync(ApplicationContext, this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void OnCreate()
|
||||
@@ -47,15 +58,17 @@ namespace Bit.Droid
|
||||
CrossCurrentActivity.Current.Init(this);
|
||||
}
|
||||
|
||||
public void OnProviderInstallFailed(int errorCode, Intent recoveryIntent)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProviderInstalled()
|
||||
{
|
||||
}
|
||||
|
||||
private void RegisterLocalServices()
|
||||
{
|
||||
ServiceContainer.Register<ILogService>("logService", new AndroidLogService());
|
||||
ServiceContainer.Register("settingsShim", new App.Migration.SettingsShim());
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
ServiceContainer.Register<App.Migration.Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService", new Migration.AndroidKeyStoreStorageService());
|
||||
}
|
||||
|
||||
Refractored.FabControl.Droid.FloatingActionButtonViewRenderer.Init();
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
@@ -73,7 +86,7 @@ namespace Bit.Droid
|
||||
CrossFingerprint.SetDialogFragmentType<CustomFingerprintDialogFragment>();
|
||||
|
||||
var preferencesStorage = new PreferencesStorageService(null);
|
||||
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
|
||||
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
||||
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
||||
liteDbStorage.InitAsync();
|
||||
var localizeService = new LocalizeService();
|
||||
@@ -84,7 +97,7 @@ namespace Bit.Droid
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService, messagingService,
|
||||
broadcasterService);
|
||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
|
||||
@@ -125,11 +138,11 @@ namespace Bit.Droid
|
||||
|
||||
private async Task BootstrapAsync()
|
||||
{
|
||||
var disableFavicon = await ServiceContainer.Resolve<IStorageService>("storageService").GetAsync<bool?>(
|
||||
Constants.DisableFaviconKey);
|
||||
await ServiceContainer.Resolve<IStateService>("stateService").SaveAsync(Constants.DisableFaviconKey,
|
||||
disableFavicon);
|
||||
var disableFavicon = await ServiceContainer.Resolve<IStorageService>("storageService")
|
||||
.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await ServiceContainer.Resolve<IStateService>("stateService").SaveAsync(
|
||||
Constants.DisableFaviconKey, disableFavicon);
|
||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Android.OS;
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Android.Security;
|
||||
using Javax.Security.Auth.X500;
|
||||
using Java.Math;
|
||||
using Android.Security.Keystore;
|
||||
using Android.App;
|
||||
using Java.Util;
|
||||
using Javax.Crypto.Spec;
|
||||
using Android.Preferences;
|
||||
using Bit.App.Migration;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Migration.Abstractions;
|
||||
|
||||
namespace Bit.Droid.Migration
|
||||
{
|
||||
public class AndroidKeyStoreStorageService : IOldSecureStorageService
|
||||
{
|
||||
private const string AndroidKeyStore = "AndroidKeyStore";
|
||||
private const string AesMode = "AES/GCM/NoPadding";
|
||||
|
||||
private const string KeyAlias = "bitwardenKey2";
|
||||
private const string KeyAliasV1 = "bitwardenKey";
|
||||
|
||||
private const string SettingsFormat = "ksSecured2:{0}";
|
||||
private const string SettingsFormatV1 = "ksSecured:{0}";
|
||||
|
||||
private const string AesKey = "ksSecured2:aesKeyForService";
|
||||
private const string AesKeyV1 = "ksSecured:aesKeyForService";
|
||||
|
||||
private readonly string _rsaMode;
|
||||
private readonly bool _oldAndroid;
|
||||
private readonly SettingsShim _settings;
|
||||
private readonly KeyStore _keyStore;
|
||||
|
||||
public AndroidKeyStoreStorageService()
|
||||
{
|
||||
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
|
||||
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
_settings = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
|
||||
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
|
||||
_keyStore.Load(null);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
GenerateStoreKey(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
GenerateStoreKey(false);
|
||||
}
|
||||
|
||||
GenerateAesKey();
|
||||
*/
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _settings.Contains(string.Format(SettingsFormat, key)) ||
|
||||
_settings.Contains(string.Format(SettingsFormatV1, key));
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
CleanupOld(key);
|
||||
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(_settings.Contains(formattedKey))
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Retrieve(string key)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(!_settings.Contains(formattedKey))
|
||||
{
|
||||
return TryGetAndMigrate(key);
|
||||
}
|
||||
|
||||
var cs = _settings.GetValueOrDefault(formattedKey, null);
|
||||
if(string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt from secure storage.");
|
||||
_settings.Remove(formattedKey);
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string key, byte[] dataBytes)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
CleanupOld(key);
|
||||
if(dataBytes == null)
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cipherString = App.Migration.Crypto.AesCbcEncrypt(dataBytes, aesKey);
|
||||
_settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to encrypt to secure storage.");
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStoreKey(bool withDate)
|
||||
{
|
||||
if(_keyStore.ContainsAlias(KeyAlias))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearSettings();
|
||||
|
||||
var end = Calendar.Instance;
|
||||
end.Add(CalendarField.Year, 99);
|
||||
|
||||
if(_oldAndroid)
|
||||
{
|
||||
var subject = new X500Principal($"CN={KeyAlias}");
|
||||
|
||||
var builder = new KeyPairGeneratorSpec.Builder(Application.Context)
|
||||
.SetAlias(KeyAlias)
|
||||
.SetSubject(subject)
|
||||
.SetSerialNumber(BigInteger.Ten);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetStartDate(new Date(0)).SetEndDate(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore);
|
||||
gen.Initialize(spec);
|
||||
gen.GenerateKeyPair();
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetKeyValidityStart(new Date(0)).SetKeyValidityEnd(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore);
|
||||
gen.Init(spec);
|
||||
gen.GenerateKey();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore.PrivateKeyEntry GetRsaKeyEntry(string alias)
|
||||
{
|
||||
return _keyStore.GetEntry(alias, null) as KeyStore.PrivateKeyEntry;
|
||||
}
|
||||
|
||||
private void GenerateAesKey()
|
||||
{
|
||||
if(_settings.Contains(AesKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = App.Migration.Crypto.RandomBytes(512 / 8);
|
||||
var encKey = _oldAndroid ? RsaEncrypt(key) : AesEncrypt(key);
|
||||
_settings.AddOrUpdateValue(AesKey, encKey);
|
||||
}
|
||||
|
||||
private App.Migration.Models.SymmetricCryptoKey GetAesKey(bool v1 = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var aesKey = v1 ? AesKeyV1 : AesKey;
|
||||
if(!_settings.Contains(aesKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var encKey = _settings.GetValueOrDefault(aesKey, null);
|
||||
if(string.IsNullOrWhiteSpace(encKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_oldAndroid || v1)
|
||||
{
|
||||
var encKeyBytes = Convert.FromBase64String(encKey);
|
||||
var key = RsaDecrypt(encKeyBytes, v1);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = encKey.Split('|');
|
||||
if(parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ivBytes = Convert.FromBase64String(parts[0]);
|
||||
var encKeyBytes = Convert.FromBase64String(parts[1]);
|
||||
var key = AesDecrypt(ivBytes, encKeyBytes);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Cannot get AesKey.");
|
||||
_keyStore.DeleteEntry(KeyAlias);
|
||||
_settings.Remove(AesKey);
|
||||
if(!v1)
|
||||
{
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string AesEncrypt(byte[] input)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry);
|
||||
var encBytes = cipher.DoFinal(input);
|
||||
var ivBytes = cipher.GetIV();
|
||||
return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}";
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] AesDecrypt(byte[] iv, byte[] encData)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
var spec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.DecryptMode, entry, spec);
|
||||
var decBytes = cipher.DoFinal(encData);
|
||||
return decBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private string RsaEncrypt(byte[] data)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey);
|
||||
var cipherText = cipher.DoFinal(data);
|
||||
return Convert.ToBase64String(cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaDecrypt(byte[] encData, bool v1)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(v1 ? KeyAliasV1 : KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
if(_oldAndroid)
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default);
|
||||
}
|
||||
|
||||
var plainText = cipher.DoFinal(encData);
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetAndMigrate(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
var aesKeyV1 = GetAesKey(true);
|
||||
if(aesKeyV1 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = _settings.GetValueOrDefault(formattedKeyV1, null);
|
||||
var value = App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKeyV1);
|
||||
Store(key, value);
|
||||
return value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt v1 from secure storage.");
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOld(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSettings(string format = SettingsFormat)
|
||||
{
|
||||
var prefix = string.Format(format, string.Empty);
|
||||
|
||||
using(var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(Application.Context))
|
||||
using(var sharedPreferencesEditor = sharedPreferences.Edit())
|
||||
{
|
||||
var removed = false;
|
||||
foreach(var pref in sharedPreferences.All)
|
||||
{
|
||||
if(pref.Key.StartsWith(prefix))
|
||||
{
|
||||
removed = true;
|
||||
sharedPreferencesEditor.Remove(pref.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if(removed)
|
||||
{
|
||||
sharedPreferencesEditor.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,19 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.0.0"
|
||||
android:versionName="2.3.0"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="29" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
@@ -51,5 +54,6 @@
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyTitle("BitwardenAndroid")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("8bit Solutions LLC")]
|
||||
[assembly: AssemblyCompany("Bitwarden Inc.")]
|
||||
[assembly: AssemblyProduct("Bitwarden")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.ClearClipboardAlarmReceiver", Exported = false)]
|
||||
public class ClearClipboardAlarmReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
public override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
||||
var lastClipboardValue = await stateService.GetAsync<string>(Constants.LastClipboardValueKey);
|
||||
await stateService.RemoveAsync(Constants.LastClipboardValueKey);
|
||||
if(lastClipboardValue == clipboardManager.Text)
|
||||
if(StaticStore.LastClipboardValue != null && StaticStore.LastClipboardValue == clipboardManager.Text)
|
||||
{
|
||||
clipboardManager.Text = string.Empty;
|
||||
}
|
||||
StaticStore.LastClipboardValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Android/Receivers/EventUploadReceiver.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Android.Content;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.EventUploadReceiver", Exported = false)]
|
||||
public class EventUploadReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
await eventService.UploadEventsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Android/Receivers/RestrictionsChangedReceiver.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.RestrictionsChangedReceiver", Exported = false)]
|
||||
[IntentFilter(new[] { Intent.ActionApplicationRestrictionsChanged })]
|
||||
public class RestrictionsChangedReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
if(intent.Action == Intent.ActionApplicationRestrictionsChanged)
|
||||
{
|
||||
await AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,14 @@ namespace Bit.Droid.Renderers
|
||||
var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.slider_thumb, null);
|
||||
if(t is GradientDrawable thumb)
|
||||
{
|
||||
thumb.SetColor(view.ThumbColor.ToAndroid());
|
||||
if(view.ThumbColor == Color.Default)
|
||||
{
|
||||
thumb.SetColor(Color.White.ToAndroid());
|
||||
}
|
||||
else
|
||||
{
|
||||
thumb.SetColor(view.ThumbColor.ToAndroid());
|
||||
}
|
||||
thumb.SetStroke(3, view.ThumbBorderColor.ToAndroid());
|
||||
Control.SetThumb(thumb);
|
||||
}
|
||||
|
||||
13324
src/Android/Resources/Resource.designer.cs
generated
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
BIN
src/Android/Resources/drawable-hdpi/accessibility_overlay.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/Android/Resources/drawable-hdpi/accessibility_permission.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
BIN
src/Android/Resources/drawable-xhdpi/accessibility_overlay.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/accessibility_overlay.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -16,4 +16,10 @@
|
||||
<string name="PasswordGenerator">
|
||||
Password Generator
|
||||
</string>
|
||||
<string name="SelfHostedServerUrl">
|
||||
Self-hosted server URL
|
||||
</string>
|
||||
<string name="ServerUrl">
|
||||
Server URL
|
||||
</string>
|
||||
</resources>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<item name="colorPrimary">@android:color/black</item>
|
||||
<item name="colorPrimaryDark">@android:color/black</item>
|
||||
<item name="colorControlNormal">@color/black_border</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<!-- Nord theme -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:summary="@string/AutoFillServiceSummary"
|
||||
android:description="@string/AutoFillServiceDescription"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewFocused"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewFocused|typeViewClicked"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagReportViewIds"
|
||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||
android:notificationTimeout="100"
|
||||
android:canRetrieveWindowContent="true"/>
|
||||
9
src/Android/Resources/xml/app_restrictions.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<restriction
|
||||
android:key="baseEnvironmentUrl"
|
||||
android:title="@string/SelfHostedServerUrl"
|
||||
android:restrictionType="string"
|
||||
android:description="@string/ServerUrl"
|
||||
android:defaultValue="" />
|
||||
</restrictions>
|
||||
@@ -48,6 +48,9 @@
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.fenix"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.fenix.nightly"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.reference.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -87,4 +90,7 @@
|
||||
<compatibility-package
|
||||
android:name="com.ecosia.android"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.vivaldi.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
</autofill-service>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="internal" path="." />
|
||||
</paths>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<!-- Trust pre-installed CAs -->
|
||||
<certificates src="system" />
|
||||
|
||||
@@ -8,9 +8,12 @@ using Android.App;
|
||||
using Android.App.Assist;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Hardware.Biometrics;
|
||||
using Android.Hardware.Fingerprints;
|
||||
using Android.Nfc;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Runtime;
|
||||
using Android.Support.V4.App;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Text;
|
||||
@@ -28,6 +31,7 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Autofill;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -36,18 +40,22 @@ namespace Bit.Droid.Services
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly Func<IEventService> _eventServiceFunc;
|
||||
private ProgressDialog _progressDialog;
|
||||
private bool _cameraPermissionsDenied;
|
||||
private Toast _toast;
|
||||
private string _userAgent;
|
||||
|
||||
public DeviceActionService(
|
||||
IStorageService storageService,
|
||||
IMessagingService messagingService,
|
||||
IBroadcasterService broadcasterService)
|
||||
IBroadcasterService broadcasterService,
|
||||
Func<IEventService> eventServiceFunc)
|
||||
{
|
||||
_storageService = storageService;
|
||||
_messagingService = messagingService;
|
||||
_broadcasterService = broadcasterService;
|
||||
_eventServiceFunc = eventServiceFunc;
|
||||
|
||||
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
||||
{
|
||||
@@ -58,6 +66,19 @@ namespace Bit.Droid.Services
|
||||
});
|
||||
}
|
||||
|
||||
public string DeviceUserAgent
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_userAgent))
|
||||
{
|
||||
_userAgent = $"Bitwarden_Mobile/{Xamarin.Essentials.AppInfo.VersionString} " +
|
||||
$"(Android {Build.VERSION.Release}; SDK {Build.VERSION.Sdk}; Model {Build.Model})";
|
||||
}
|
||||
return _userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Android;
|
||||
|
||||
public void Toast(string text, bool longDuration = false)
|
||||
@@ -111,38 +132,14 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
if(!CanOpenFile(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
activity.StartActivity(intent);
|
||||
return true;
|
||||
}
|
||||
@@ -151,22 +148,105 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||
PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||
{
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
intent.SetType(mimeType);
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
return intent;
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
|
||||
if(contentUri != null)
|
||||
{
|
||||
var uri = Android.Net.Uri.Parse(contentUri);
|
||||
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
||||
// Using java bufferedOutputStream due to this issue:
|
||||
// https://github.com/xamarin/xamarin-android/issues/3498
|
||||
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
||||
javaStream.Write(fileData);
|
||||
javaStream.Flush();
|
||||
javaStream.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prompt for location to save file
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
// Unable to identify so fall back to generic "any" type
|
||||
mimeType = "*/*";
|
||||
}
|
||||
|
||||
var intent = new Intent(Intent.ActionCreateDocument);
|
||||
intent.SetType(mimeType);
|
||||
intent.AddCategory(Intent.CategoryOpenable);
|
||||
intent.PutExtra(Intent.ExtraTitle, fileName);
|
||||
|
||||
activity.StartActivityForResult(intent, Constants.SaveFileRequestCode);
|
||||
return true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
@@ -182,7 +262,8 @@ namespace Bit.Droid.Services
|
||||
public Task SelectFileAsync()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
||||
HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var additionalIntents = new List<IParcelable>();
|
||||
if(activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
||||
{
|
||||
@@ -201,14 +282,14 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||
if(!file.Exists())
|
||||
{
|
||||
file.ParentFile.Mkdirs();
|
||||
file.CreateNewFile();
|
||||
}
|
||||
var outputFileUri = Android.Net.Uri.FromFile(file);
|
||||
var outputFileUri = FileProvider.GetUriForFile(activity,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
||||
}
|
||||
catch(Java.IO.IOException) { }
|
||||
@@ -306,11 +387,72 @@ namespace Bit.Droid.Services
|
||||
Application.Context.PackageName, 0).VersionCode.ToString();
|
||||
}
|
||||
|
||||
public bool SupportsFaceId()
|
||||
public bool SupportsFaceBiometric()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> SupportsFaceBiometricAsync()
|
||||
{
|
||||
return Task.FromResult(SupportsFaceBiometric());
|
||||
}
|
||||
|
||||
public async Task<bool> BiometricAvailableAsync()
|
||||
{
|
||||
if(UseNativeBiometric())
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var manager = activity.GetSystemService(Context.BiometricService) as BiometricManager;
|
||||
return manager.CanAuthenticate() == BiometricCode.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CrossFingerprint.Current.IsAvailableAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseNativeBiometric()
|
||||
{
|
||||
return (int)Build.VERSION.SdkInt >= 29;
|
||||
}
|
||||
|
||||
public Task<bool> AuthenticateBiometricAsync(string text = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
text = AppResources.BiometricsDirection;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
using(var builder = new BiometricPrompt.Builder(activity))
|
||||
{
|
||||
builder.SetTitle(text);
|
||||
builder.SetConfirmationRequired(false);
|
||||
builder.SetNegativeButton(AppResources.Cancel, activity.MainExecutor,
|
||||
new DialogInterfaceOnClickListener
|
||||
{
|
||||
Clicked = () => { }
|
||||
});
|
||||
var prompt = builder.Build();
|
||||
var result = new TaskCompletionSource<bool>();
|
||||
prompt.Authenticate(new CancellationSignal(), activity.MainExecutor,
|
||||
new BiometricAuthenticationCallback
|
||||
{
|
||||
Success = authResult => result.TrySetResult(true),
|
||||
Failed = () => result.TrySetResult(false),
|
||||
Help = (helpCode, helpString) => { }
|
||||
});
|
||||
return result.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsNfc()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
@@ -463,6 +605,7 @@ namespace Bit.Droid.Services
|
||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
||||
activity.SetResult(Result.Ok, replyIntent);
|
||||
activity.Finish();
|
||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -488,6 +631,10 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
activity.Finish();
|
||||
_messagingService.Send("finishMainActivity");
|
||||
if(cipher != null)
|
||||
{
|
||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +673,40 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillAccessibilityOverlayPermitted()
|
||||
{
|
||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||
}
|
||||
|
||||
public void OpenAccessibilityOverlayPermissionSettings()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Settings.ActionManageOverlayPermission);
|
||||
intent.SetData(Android.Net.Uri.Parse("package:com.x8bit.bitwarden"));
|
||||
activity.StartActivity(intent);
|
||||
}
|
||||
catch(ActivityNotFoundException)
|
||||
{
|
||||
// can't open overlay permission management, fall back to app settings
|
||||
var intent = new Intent(Settings.ActionApplicationDetailsSettings);
|
||||
intent.SetData(Android.Net.Uri.Parse("package:com.x8bit.bitwarden"));
|
||||
activity.StartActivity(intent);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var alertBuilder = new AlertDialog.Builder(activity);
|
||||
alertBuilder.SetMessage(AppResources.BitwardenAutofillGoToSettings);
|
||||
alertBuilder.SetCancelable(true);
|
||||
alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) =>
|
||||
{
|
||||
(sender as AlertDialog)?.Cancel();
|
||||
});
|
||||
alertBuilder.Create().Show();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillServiceEnabled()
|
||||
{
|
||||
if(Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||
@@ -535,7 +716,8 @@ namespace Bit.Droid.Services
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var afm = (AutofillManager)activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
var afm = (AutofillManager)activity.GetSystemService(
|
||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||
}
|
||||
catch
|
||||
@@ -577,6 +759,11 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool UsingDarkTheme()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if(dir != null && dir.IsDirectory)
|
||||
@@ -651,15 +838,19 @@ namespace Bit.Droid.Services
|
||||
|
||||
private async Task CopyTotpAsync(CipherView cipher)
|
||||
{
|
||||
var autoCopyDisabled = await _storageService.GetAsync<bool?>(Constants.DisableAutoTotpCopyKey);
|
||||
var canAccessPremium = await ServiceContainer.Resolve<IUserService>("userService").CanAccessPremiumAsync();
|
||||
if((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault() &&
|
||||
!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
||||
if(!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
||||
{
|
||||
var totp = await ServiceContainer.Resolve<ITotpService>("totpService").GetCodeAsync(cipher.Login.Totp);
|
||||
if(totp != null)
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
var autoCopyDisabled = await _storageService.GetAsync<bool?>(Constants.DisableAutoTotpCopyKey);
|
||||
var canAccessPremium = await userService.CanAccessPremiumAsync();
|
||||
if((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
||||
{
|
||||
CopyToClipboard(totp);
|
||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
||||
if(totp != null)
|
||||
{
|
||||
CopyToClipboard(totp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,5 +862,41 @@ namespace Bit.Droid.Services
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
clipboardManager.Text = text;
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback
|
||||
{
|
||||
public Action<BiometricPrompt.AuthenticationResult> Success { get; set; }
|
||||
public Action Failed { get; set; }
|
||||
public Action<BiometricAcquiredStatus, Java.Lang.ICharSequence> Help { get; set; }
|
||||
|
||||
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult)
|
||||
{
|
||||
base.OnAuthenticationSucceeded(authResult);
|
||||
Success?.Invoke(authResult);
|
||||
}
|
||||
|
||||
public override void OnAuthenticationFailed()
|
||||
{
|
||||
base.OnAuthenticationFailed();
|
||||
Failed?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnAuthenticationHelp([GeneratedEnum] BiometricAcquiredStatus helpCode,
|
||||
Java.Lang.ICharSequence helpString)
|
||||
{
|
||||
base.OnAuthenticationHelp(helpCode, helpString);
|
||||
Help?.Invoke(helpCode, helpString);
|
||||
}
|
||||
}
|
||||
|
||||
private class DialogInterfaceOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
|
||||
{
|
||||
public Action Clicked { get; set; }
|
||||
|
||||
public void OnClick(IDialogInterface dialog, int which)
|
||||
{
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ namespace Bit.Droid.Services
|
||||
netLanguage = "zh-Hans";
|
||||
}
|
||||
}
|
||||
else if(androidLanguage.StartsWith("iw"))
|
||||
{
|
||||
// Uncomment when we support RTL
|
||||
// netLanguage = "he";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Certain languages need to be converted to CultureInfo equivalent
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using Android.Content;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Content;
|
||||
using Android.Provider;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public static class AndroidHelpers
|
||||
{
|
||||
private static string BaseEnvironmentUrlRestrictionKey = "baseEnvironmentUrl";
|
||||
|
||||
public static string GetFileName(Context context, Android.Net.Uri uri)
|
||||
{
|
||||
string name = null;
|
||||
@@ -26,5 +31,21 @@ namespace Bit.Droid.Utilities
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public static async Task SetPreconfiguredRestrictionSettingsAsync(Context context)
|
||||
{
|
||||
var restrictionsManager = (RestrictionsManager)context.GetSystemService(Context.RestrictionsService);
|
||||
var restrictions = restrictionsManager.ApplicationRestrictions;
|
||||
var dict = new Dictionary<string, string>();
|
||||
if(restrictions.ContainsKey(BaseEnvironmentUrlRestrictionKey))
|
||||
{
|
||||
dict.Add(BaseEnvironmentUrlRestrictionKey, restrictions.GetString(BaseEnvironmentUrlRestrictionKey));
|
||||
}
|
||||
|
||||
if(dict.Count > 0)
|
||||
{
|
||||
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ using Newtonsoft.Json;
|
||||
using Android.Runtime;
|
||||
using Bit.Core.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Content;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public class HockeyAppCrashManagerListener : CrashManagerListener
|
||||
{
|
||||
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
|
||||
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
@@ -31,10 +34,11 @@ namespace Bit.Droid.Utilities
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
public async Task InitAsync(Context context)
|
||||
{
|
||||
_userId = await _userService.GetUserIdAsync();
|
||||
_appId = await _appIdService.GetAppIdAsync();
|
||||
CrashManager.Register(context, HockeyAppId, this);
|
||||
}
|
||||
|
||||
public override string Description
|
||||
|
||||
19
src/Android/Utilities/StaticStore.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public static class StaticStore
|
||||
{
|
||||
public static string LastClipboardValue { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
$androidPath = $($rootPath + "\src\Android\Android.csproj");
|
||||
$appPath = $($rootPath + "\src\App\App.csproj");
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Increment Version"
|
||||
echo "########################################"
|
||||
|
||||
$androidManifest = $($rootPath + "\src\Android\Properties\AndroidManifest.xml");
|
||||
|
||||
@@ -15,35 +17,73 @@ $node.SetAttribute("android:versionCode", $env:APPVEYOR_BUILD_NUMBER);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Decrypt Keystore"
|
||||
echo "########################################"
|
||||
|
||||
$encKeystorePath = $($rootPath + "\src\Android\8bit.keystore.enc");
|
||||
$secureFilePath = $($rootPath + "\secure-file\tools\secure-file.exe");
|
||||
|
||||
Invoke-Expression "& `"$secureFilePath`" -decrypt $($encKeystorePath) -secret $($env:keystore_dec_secret)"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Sign Release Configuration"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" "/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:keystore_password)" "/p:AndroidSigningKeyStore=8bit.keystore" "/p:AndroidSigningStorePass=$($env:keystore_password)" "/v:quiet"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:keystore_password)" `
|
||||
"/p:AndroidSigningKeyStore=8bit.keystore" "/p:AndroidSigningStorePass=$($env:keystore_password)" "/v:quiet"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Copy Release apk to project root"
|
||||
echo "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "\src\Android\bin\Release\com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "\com.x8bit.bitwarden.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Clean Android and App"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
msbuild "$($appPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Backup project files"
|
||||
echo "########################################"
|
||||
|
||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||
Copy-Item $androidPath $($androidPath + ".original");
|
||||
Copy-Item $appPath $($appPath + ".original");
|
||||
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Cleanup Android Manifest"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
|
||||
$nsAndroid=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$nsAndroid.AddNamespace("android", "http://schemas.android.com/apk/res/android");
|
||||
|
||||
$firebaseReceiver1=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdInternalReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver1.ParentNode.RemoveChild($firebaseReceiver1);
|
||||
|
||||
$firebaseReceiver2=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver2.ParentNode.RemoveChild($firebaseReceiver2);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Uninstall from Android.csproj"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidPath);
|
||||
@@ -51,12 +91,19 @@ $xml.Load($androidPath);
|
||||
$ns=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI);
|
||||
|
||||
$firebaseNode=$xml.SelectSingleNode("/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns);
|
||||
$firebaseNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns);
|
||||
$firebaseNode.ParentNode.RemoveChild($firebaseNode);
|
||||
|
||||
$safetyNetNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.GooglePlayServices.SafetyNet']", $ns);
|
||||
$safetyNetNode.ParentNode.RemoveChild($safetyNetNode);
|
||||
|
||||
$xml.Save($androidPath);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Uninstall from App.csproj"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($appPath);
|
||||
@@ -66,22 +113,31 @@ $hockeyNode.ParentNode.RemoveChild($hockeyNode);
|
||||
|
||||
$xml.Save($appPath);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Restore NuGet"
|
||||
|
||||
$nugetPath = $($rootPath + "\nuget.exe");
|
||||
echo "########################################"
|
||||
|
||||
Invoke-Expression "& nuget restore"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Build and Sign FDroid Configuration"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" "/p:Configuration=FDroid"
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=FDroid" "/p:AndroidKeyStore=true" "/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:keystore_password)" "/p:AndroidSigningKeyStore=8bit.keystore" "/p:AndroidSigningStorePass=$($env:keystore_password)" "/v:quiet"
|
||||
|
||||
msbuild "$($androidPath)" "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" `
|
||||
"/p:Configuration=FDroid"
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=FDroid" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:keystore_password)" `
|
||||
"/p:AndroidSigningKeyStore=8bit.keystore" "/p:AndroidSigningStorePass=$($env:keystore_password)" "/v:quiet"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Copy FDroid apk to project root"
|
||||
echo "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "\src\Android\bin\FDroid\com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "\com.x8bit.bitwarden-fdroid.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Done"
|
||||
echo "########################################"
|
||||
|
||||
@@ -6,12 +6,14 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDeviceActionService
|
||||
{
|
||||
string DeviceUserAgent { get; }
|
||||
DeviceType DeviceType { get; }
|
||||
void Toast(string text, bool longDuration = false);
|
||||
bool LaunchApp(string appName);
|
||||
Task ShowLoadingAsync(string text);
|
||||
Task HideLoadingAsync();
|
||||
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||
bool SaveFile(byte[] fileData, string id, string fileName, string contentUri);
|
||||
bool CanOpenFile(string fileName);
|
||||
Task ClearCacheAsync();
|
||||
Task SelectFileAsync();
|
||||
@@ -19,7 +21,11 @@ namespace Bit.App.Abstractions
|
||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||
bool autofocus = true);
|
||||
void RateApp();
|
||||
bool SupportsFaceId();
|
||||
bool SupportsFaceBiometric();
|
||||
Task<bool> SupportsFaceBiometricAsync();
|
||||
Task<bool> BiometricAvailableAsync();
|
||||
bool UseNativeBiometric();
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null);
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsAutofillService();
|
||||
@@ -30,9 +36,12 @@ namespace Bit.App.Abstractions
|
||||
void CloseAutofill();
|
||||
void Background();
|
||||
bool AutofillAccessibilityServiceRunning();
|
||||
bool AutofillAccessibilityOverlayPermitted();
|
||||
bool AutofillServiceEnabled();
|
||||
string GetBuildNumber();
|
||||
void OpenAccessibilitySettings();
|
||||
void OpenAccessibilityOverlayPermissionSettings();
|
||||
void OpenAutofillSettings();
|
||||
bool UsingDarkTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<PackageReference Include="HockeySDK.Xamarin" Version="5.2.0" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="1.4.9" />
|
||||
<PackageReference Include="Refractored.FloatingActionButtonForms" Version="2.1.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.3.1" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="4.4.0.991265" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.1.47" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
<Compile Update="Pages\Generator\GeneratorHistoryPage.xaml.cs">
|
||||
<DependentUpon>GeneratorHistoryPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\AutofillPage.xaml.cs">
|
||||
<DependentUpon>AutofillPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
|
||||
<DependentUpon>ExtensionPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\AutofillServicePage.xaml.cs">
|
||||
<DependentUpon>AutofillServicePage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -60,6 +66,9 @@
|
||||
<Compile Update="Pages\Settings\FoldersPage.xaml.cs">
|
||||
<DependentUpon>FoldersPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\ExportVaultPage.xaml.cs">
|
||||
<DependentUpon>ExportVaultPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\OptionsPage.xaml.cs">
|
||||
<DependentUpon>OptionsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace Bit.App
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private static bool _isResumed;
|
||||
|
||||
public App(AppOptions appOptions)
|
||||
{
|
||||
_appOptions = appOptions ?? new AppOptions();
|
||||
@@ -89,9 +91,7 @@ namespace Bit.App
|
||||
}
|
||||
else if(message.Command == "locked")
|
||||
{
|
||||
await _stateService.PurgeAsync();
|
||||
var lockPage = new LockPage(_appOptions, !(message.Data as bool?).GetValueOrDefault());
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
await LockedAsync(!(message.Data as bool?).GetValueOrDefault());
|
||||
}
|
||||
else if(message.Command == "lockVault")
|
||||
{
|
||||
@@ -99,11 +99,8 @@ namespace Bit.App
|
||||
}
|
||||
else if(message.Command == "logout")
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(false));
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
await LogOutAsync((message.Data as bool?).GetValueOrDefault()));
|
||||
}
|
||||
else if(message.Command == "loggedOut")
|
||||
{
|
||||
@@ -114,7 +111,14 @@ namespace Bit.App
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
SyncIfNeeded();
|
||||
ResumedAsync();
|
||||
}
|
||||
}
|
||||
else if(message.Command == "slept")
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
}
|
||||
else if(message.Command == "migrated")
|
||||
@@ -148,11 +152,33 @@ namespace Bit.App
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/7478
|
||||
// Fixed in last Xamarin.Forms 4.4.0.x - remove this hack after updating
|
||||
public static void WaitForResume()
|
||||
{
|
||||
var checkFrequencyInMillis = 100;
|
||||
var maxTimeInMillis = 5000;
|
||||
|
||||
var count = 0;
|
||||
while(!_isResumed)
|
||||
{
|
||||
Task.Delay(checkFrequencyInMillis).Wait();
|
||||
count += checkFrequencyInMillis;
|
||||
|
||||
// don't let this run forever
|
||||
if(count >= maxTimeInMillis)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnStart()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||
await ClearCacheIfNeededAsync();
|
||||
await TryClearCiphersCacheAsync();
|
||||
Prime();
|
||||
if(string.IsNullOrWhiteSpace(_appOptions.Uri))
|
||||
{
|
||||
@@ -163,31 +189,58 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
}
|
||||
}
|
||||
_messagingService.Send("startEventTimer");
|
||||
}
|
||||
|
||||
protected async override void OnSleep()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnSleep");
|
||||
_isResumed = false;
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow);
|
||||
var isLocked = await _lockService.IsLockedAsync();
|
||||
if(!isLocked)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow);
|
||||
}
|
||||
SetTabsPageFromAutofill(isLocked);
|
||||
await SleptAsync();
|
||||
}
|
||||
SetTabsPageFromAutofill();
|
||||
await HandleLockingAsync();
|
||||
}
|
||||
|
||||
protected async override void OnResume()
|
||||
protected override void OnResume()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||
_messagingService.Send("cancelLockTimer");
|
||||
await ClearCacheIfNeededAsync();
|
||||
Prime();
|
||||
_isResumed = true;
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
SyncIfNeeded();
|
||||
ResumedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SleptAsync()
|
||||
{
|
||||
await HandleLockingAsync();
|
||||
_messagingService.Send("stopEventTimer");
|
||||
}
|
||||
|
||||
private async void ResumedAsync()
|
||||
{
|
||||
_messagingService.Send("cancelLockTimer");
|
||||
_messagingService.Send("startEventTimer");
|
||||
await ClearCacheIfNeededAsync();
|
||||
await TryClearCiphersCacheAsync();
|
||||
Prime();
|
||||
SyncIfNeeded();
|
||||
if(Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/7478
|
||||
await Task.Delay(100);
|
||||
Current.MainPage = new NavigationPage(lockPage);
|
||||
// End workaround
|
||||
}
|
||||
await lockPage.PromptFingerprintAfterResumeAsync();
|
||||
}
|
||||
}
|
||||
@@ -213,13 +266,17 @@ namespace Bit.App
|
||||
_folderService.ClearAsync(userId),
|
||||
_collectionService.ClearAsync(userId),
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_lockService.ClearAsync());
|
||||
_lockService.PinLocked = false;
|
||||
_lockService.ClearAsync(),
|
||||
_stateService.PurgeAsync());
|
||||
_lockService.FingerprintLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
Current.MainPage = new HomePage();
|
||||
if(expired)
|
||||
{
|
||||
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,7 +344,7 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTabsPageFromAutofill()
|
||||
private void SetTabsPageFromAutofill(bool isLocked)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(_appOptions.Uri) &&
|
||||
!_appOptions.FromAutofillFramework)
|
||||
@@ -296,8 +353,15 @@ namespace Bit.App
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Current.MainPage = new TabsPage();
|
||||
_appOptions.Uri = null;
|
||||
if(isLocked)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LockPage());
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new TabsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -316,7 +380,7 @@ namespace Bit.App
|
||||
{
|
||||
InitializeComponent();
|
||||
SetCulture();
|
||||
ThemeManager.SetTheme();
|
||||
ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android);
|
||||
Current.MainPage = new HomePage();
|
||||
var mainPageTask = SetMainPageAsync();
|
||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||
@@ -324,10 +388,6 @@ namespace Bit.App
|
||||
|
||||
private void SyncIfNeeded()
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
return;
|
||||
@@ -335,11 +395,71 @@ namespace Bit.App
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if(DateTime.UtcNow - lastSync > TimeSpan.FromMinutes(30))
|
||||
if(lastSync == null || ((DateTime.UtcNow - lastSync) > TimeSpan.FromMinutes(30)))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _syncService.FullSyncAsync(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TryClearCiphersCacheAsync()
|
||||
{
|
||||
if(Device.RuntimePlatform != Device.iOS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var clearCache = await _storageService.GetAsync<bool?>(Constants.ClearCiphersCacheKey);
|
||||
if(clearCache.GetValueOrDefault())
|
||||
{
|
||||
_cipherService.ClearCache();
|
||||
await _storageService.RemoveAsync(Constants.ClearCiphersCacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LockedAsync(bool autoPromptFingerprint)
|
||||
{
|
||||
await _stateService.PurgeAsync();
|
||||
if(autoPromptFingerprint && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var lockOptions = await _storageService.GetAsync<int?>(Constants.LockOptionKey);
|
||||
if(lockOptions == 0)
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
}
|
||||
}
|
||||
else if(autoPromptFingerprint && Device.RuntimePlatform == Device.Android &&
|
||||
_deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
}
|
||||
PreviousPageInfo lastPageBeforeLock = null;
|
||||
if(Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||
if(topPage is NavigationPage navPage)
|
||||
{
|
||||
if(navPage.CurrentPage is ViewPage viewPage)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "view",
|
||||
CipherId = viewPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
else if(navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "edit",
|
||||
CipherId = addEditPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
await _storageService.SaveAsync(Constants.PreviousPageKey, lastPageBeforeLock);
|
||||
var lockPage = new LockPage(_appOptions, autoPromptFingerprint);
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,84 +3,112 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.CipherViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms">
|
||||
<Grid x:Name="_grid"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
|
||||
<Grid
|
||||
x:Name="_grid"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
|
||||
<Grid.BindingContext>
|
||||
<controls:CipherViewCellViewModel />
|
||||
</Grid.BindingContext>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:FaLabel x:Name="_icon"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform" />
|
||||
<ff:CachedImage x:Name="_image"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
IsVisible="False"/>
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
<controls:FaLabel
|
||||
x:Name="_icon"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<ff:CachedImage
|
||||
x:Name="_image"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
IsVisible="False"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name, Mode=OneWay}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle, Mode=OneWay}" />
|
||||
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneWay}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneWay}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Shared}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
Text=""
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="ImageButton_Clicked"
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</ViewCell>
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Bit.App.Controls
|
||||
return new Tuple<string, string>(icon, image);
|
||||
}
|
||||
|
||||
private void ImageButton_Clicked(object sender, EventArgs e)
|
||||
private void MoreButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
ButtonCommand?.Execute(Cipher);
|
||||
}
|
||||
|
||||
21
src/App/Controls/ExtendedSearchBar.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedSearchBar : SearchBar
|
||||
{
|
||||
public ExtendedSearchBar()
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService", true);
|
||||
if(!deviceActionService?.UsingDarkTheme() ?? false)
|
||||
{
|
||||
TextColor = Color.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Bit.App.Migration.Abstractions
|
||||
{
|
||||
public interface IOldSecureStorageService
|
||||
{
|
||||
bool Contains(string key);
|
||||
void Delete(string key);
|
||||
byte[] Retrieve(string key);
|
||||
void Store(string key, byte[] dataBytes);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
using Bit.App.Migration.Models;
|
||||
using Bit.Core.Enums;
|
||||
using PCLCrypto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class Crypto
|
||||
{
|
||||
public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
return new CipherString(parts.Item1, Convert.ToBase64String(parts.Item2),
|
||||
Convert.ToBase64String(parts.Item4), parts.Item3 != null ? Convert.ToBase64String(parts.Item3) : null);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcEncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
var macLength = parts.Item3?.Length ?? 0;
|
||||
|
||||
var encBytes = new byte[1 + parts.Item2.Length + macLength + parts.Item4.Length];
|
||||
encBytes[0] = (byte)parts.Item1;
|
||||
parts.Item2.CopyTo(encBytes, 1);
|
||||
if(parts.Item3 != null)
|
||||
{
|
||||
parts.Item3.CopyTo(encBytes, 1 + parts.Item2.Length);
|
||||
}
|
||||
parts.Item4.CopyTo(encBytes, 1 + parts.Item2.Length + macLength);
|
||||
return encBytes;
|
||||
}
|
||||
|
||||
private static Tuple<EncryptionType, byte[], byte[], byte[]> AesCbcEncryptToParts(byte[] plainBytes,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(plainBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(plainBytes));
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var iv = RandomBytes(provider.BlockLength);
|
||||
var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null;
|
||||
|
||||
return new Tuple<EncryptionType, byte[], byte[], byte[]>(key.EncryptionType, iv, mac, ct);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key)
|
||||
{
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
return AesCbcDecrypt(encyptedValue.EncryptionType, encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes, encyptedValue.MacBytes, key);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(EncryptionType type, byte[] ct, byte[] iv, byte[] mac,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(ct == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ct));
|
||||
}
|
||||
|
||||
if(iv == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(iv));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mac));
|
||||
}
|
||||
|
||||
if(key.EncryptionType != type)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(type));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac != null)
|
||||
{
|
||||
var computedMacBytes = ComputeMac(ct, iv, key.MacKey);
|
||||
if(!MacsEqual(computedMacBytes, mac))
|
||||
{
|
||||
throw new InvalidOperationException("MAC failed.");
|
||||
}
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, ct, iv);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
public static byte[] RandomBytes(int length)
|
||||
{
|
||||
return WinRTCrypto.CryptographicBuffer.GenerateRandom(length);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
|
||||
{
|
||||
if(ctBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ctBytes));
|
||||
}
|
||||
|
||||
if(ivBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ivBytes));
|
||||
}
|
||||
|
||||
return ComputeMac(ivBytes.Concat(ctBytes), macKey);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(IEnumerable<byte> dataBytes, byte[] macKey)
|
||||
{
|
||||
if(macKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(macKey));
|
||||
}
|
||||
|
||||
if(dataBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataBytes));
|
||||
}
|
||||
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(macKey);
|
||||
hasher.Append(dataBytes.ToArray());
|
||||
var mac = hasher.GetValueAndReset();
|
||||
return mac;
|
||||
}
|
||||
|
||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
public static bool MacsEqual(byte[] mac1, byte[] mac2)
|
||||
{
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(RandomBytes(32));
|
||||
|
||||
hasher.Append(mac1);
|
||||
mac1 = hasher.GetValueAndReset();
|
||||
|
||||
hasher.Append(mac2);
|
||||
mac2 = hasher.GetValueAndReset();
|
||||
|
||||
if(mac1.Length != mac2.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int i = 0; i < mac2.Length; i++)
|
||||
{
|
||||
if(mac1[i] != mac2[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
public static byte[] HkdfExpand(byte[] prk, byte[] info, int size)
|
||||
{
|
||||
var hashLen = 32; // sha256
|
||||
var okm = new byte[size];
|
||||
var previousT = new byte[0];
|
||||
var n = (int)Math.Ceiling((double)size / hashLen);
|
||||
for(int i = 0; i < n; i++)
|
||||
{
|
||||
var t = new byte[previousT.Length + info.Length + 1];
|
||||
previousT.CopyTo(t, 0);
|
||||
info.CopyTo(t, previousT.Length);
|
||||
t[t.Length - 1] = (byte)(i + 1);
|
||||
previousT = ComputeMac(t, prk);
|
||||
previousT.CopyTo(okm, i * hashLen);
|
||||
}
|
||||
return okm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class MigrationHelpers
|
||||
{
|
||||
public static bool Migrating = false;
|
||||
|
||||
public static bool NeedsMigration()
|
||||
{
|
||||
return ServiceContainer.Resolve<SettingsShim>("settingsShim")
|
||||
.GetValueOrDefault(Constants.OldUserIdKey, null) != null; ;
|
||||
}
|
||||
|
||||
public static async Task<bool> PerformMigrationAsync()
|
||||
{
|
||||
if(!NeedsMigration() || Migrating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Migrating = true;
|
||||
var settingsShim = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
var oldSecureStorageService = ServiceContainer.Resolve<Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService");
|
||||
|
||||
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
var syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
|
||||
// Get old data
|
||||
|
||||
var oldTokenBytes = oldSecureStorageService.Retrieve("accessToken");
|
||||
var oldToken = oldTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTokenBytes, 0, oldTokenBytes.Length);
|
||||
var oldKeyBytes = oldSecureStorageService.Retrieve("key");
|
||||
var oldKey = oldKeyBytes == null ? null : new Models.SymmetricCryptoKey(oldKeyBytes);
|
||||
var oldUserId = settingsShim.GetValueOrDefault("userId", null);
|
||||
|
||||
var isAuthenticated = oldKey != null && !string.IsNullOrWhiteSpace(oldToken) &&
|
||||
!string.IsNullOrWhiteSpace(oldUserId);
|
||||
if(!isAuthenticated)
|
||||
{
|
||||
Migrating = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldRefreshTokenBytes = oldSecureStorageService.Retrieve("refreshToken");
|
||||
var oldRefreshToken = oldRefreshTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldRefreshTokenBytes, 0, oldRefreshTokenBytes.Length);
|
||||
var oldPinBytes = oldSecureStorageService.Retrieve("pin");
|
||||
var oldPin = oldPinBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldPinBytes, 0, oldPinBytes.Length);
|
||||
|
||||
var oldEncKey = settingsShim.GetValueOrDefault("encKey", null);
|
||||
var oldEncPrivateKey = settingsShim.GetValueOrDefault("encPrivateKey", null);
|
||||
var oldEmail = settingsShim.GetValueOrDefault("email", null);
|
||||
var oldKdf = (KdfType)settingsShim.GetValueOrDefault("kdf", (int)KdfType.PBKDF2_SHA256);
|
||||
var oldKdfIterations = settingsShim.GetValueOrDefault("kdfIterations", 5000);
|
||||
|
||||
var oldTwoFactorTokenBytes = oldSecureStorageService.Retrieve(
|
||||
string.Format("twoFactorToken_{0}", Convert.ToBase64String(Encoding.UTF8.GetBytes(oldEmail))));
|
||||
var oldTwoFactorToken = oldTwoFactorTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTwoFactorTokenBytes, 0, oldTwoFactorTokenBytes.Length);
|
||||
|
||||
var oldAppIdBytes = oldSecureStorageService.Retrieve("appId");
|
||||
var oldAppId = oldAppIdBytes == null ? null : new Guid(oldAppIdBytes).ToString();
|
||||
var oldAnonAppIdBytes = oldSecureStorageService.Retrieve("anonymousAppId");
|
||||
var oldAnonAppId = oldAnonAppIdBytes == null ? null : new Guid(oldAnonAppIdBytes).ToString();
|
||||
var oldFingerprint = settingsShim.GetValueOrDefault("setting:fingerprintUnlockOn", false);
|
||||
|
||||
// Save settings
|
||||
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPersistNotificationKey,
|
||||
settingsShim.GetValueOrDefault("setting:persistNotification", false));
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPasswordFieldKey,
|
||||
settingsShim.GetValueOrDefault("setting:autofillPasswordField", false));
|
||||
await storageService.SaveAsync(Constants.DisableAutoTotpCopyKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableAutoCopyTotp", false));
|
||||
await storageService.SaveAsync(Constants.DisableFaviconKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableWebsiteIcons", false));
|
||||
await storageService.SaveAsync(Constants.AddSitePromptShownKey,
|
||||
settingsShim.GetValueOrDefault("addedSiteAlert", false));
|
||||
await storageService.SaveAsync(Constants.PushInitialPromptShownKey,
|
||||
settingsShim.GetValueOrDefault("push:initialPromptShown", false));
|
||||
await storageService.SaveAsync(Constants.PushCurrentTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:currentToken", null));
|
||||
await storageService.SaveAsync(Constants.PushRegisteredTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:registeredToken", null));
|
||||
// For some reason "push:lastRegistrationDate" isn't getting pulled from settingsShim correctly.
|
||||
// We don't really need it anyways.
|
||||
// var lastReg = settingsShim.GetValueOrDefault("push:lastRegistrationDate", DateTime.MinValue);
|
||||
// await storageService.SaveAsync(Constants.PushLastRegistrationDateKey, lastReg);
|
||||
await storageService.SaveAsync("rememberedEmail",
|
||||
settingsShim.GetValueOrDefault("other:lastLoginEmail", null));
|
||||
|
||||
await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||
{
|
||||
Base = settingsShim.GetValueOrDefault("other:baseUrl", null),
|
||||
Api = settingsShim.GetValueOrDefault("other:apiUrl", null),
|
||||
WebVault = settingsShim.GetValueOrDefault("other:webVaultUrl", null),
|
||||
Identity = settingsShim.GetValueOrDefault("other:identityUrl", null),
|
||||
Icons = settingsShim.GetValueOrDefault("other:iconsUrl", null)
|
||||
});
|
||||
|
||||
await passwordGenerationService.SaveOptionsAsync(new Core.Models.Domain.PasswordGenerationOptions
|
||||
{
|
||||
Ambiguous = settingsShim.GetValueOrDefault("pwGenerator:ambiguous", false),
|
||||
Length = settingsShim.GetValueOrDefault("pwGenerator:length", 15),
|
||||
Uppercase = settingsShim.GetValueOrDefault("pwGenerator:uppercase", true),
|
||||
Lowercase = settingsShim.GetValueOrDefault("pwGenerator:lowercase", true),
|
||||
Number = settingsShim.GetValueOrDefault("pwGenerator:numbers", true),
|
||||
MinNumber = settingsShim.GetValueOrDefault("pwGenerator:minNumbers", 0),
|
||||
Special = settingsShim.GetValueOrDefault("pwGenerator:special", true),
|
||||
MinSpecial = settingsShim.GetValueOrDefault("pwGenerator:minSpecial", 0),
|
||||
WordSeparator = "-",
|
||||
NumWords = 3
|
||||
});
|
||||
|
||||
// Save lock options
|
||||
|
||||
int? lockOptionsSeconds = settingsShim.GetValueOrDefault("setting:lockSeconds", -10);
|
||||
if(lockOptionsSeconds == -10)
|
||||
{
|
||||
lockOptionsSeconds = 60 * 15;
|
||||
}
|
||||
else if(lockOptionsSeconds == -1)
|
||||
{
|
||||
lockOptionsSeconds = null;
|
||||
}
|
||||
await storageService.SaveAsync(Constants.LockOptionKey,
|
||||
lockOptionsSeconds == null ? (int?)null : lockOptionsSeconds.Value / 60);
|
||||
|
||||
// Save app ids
|
||||
|
||||
await storageService.SaveAsync("appId", oldAppId);
|
||||
await storageService.SaveAsync("anonymousAppId", oldAnonAppId);
|
||||
|
||||
// Save new authed data
|
||||
|
||||
await tokenService.SetTwoFactorTokenAsync(oldTwoFactorToken, oldEmail);
|
||||
await tokenService.SetTokensAsync(oldToken, oldRefreshToken);
|
||||
await userService.SetInformationAsync(oldUserId, oldEmail, oldKdf, oldKdfIterations);
|
||||
|
||||
var newKey = new Core.Models.Domain.SymmetricCryptoKey(oldKey.Key);
|
||||
await cryptoService.SetKeyAsync(newKey);
|
||||
// Key hash is unavailable in old version, store old key until we can move it to key hash
|
||||
await secureStorageService.SaveAsync("oldKey", newKey.KeyB64);
|
||||
await cryptoService.SetEncKeyAsync(oldEncKey);
|
||||
await cryptoService.SetEncPrivateKeyAsync(oldEncPrivateKey);
|
||||
|
||||
// Save fingerprint/pin
|
||||
|
||||
if(oldFingerprint)
|
||||
{
|
||||
await storageService.SaveAsync(Constants.FingerprintUnlockKey, true);
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(oldPin))
|
||||
{
|
||||
var pinKey = await cryptoService.MakePinKeyAysnc(oldPin, oldEmail, oldKdf, oldKdfIterations);
|
||||
var pinProtectedKey = await cryptoService.EncryptAsync(oldKeyBytes, pinKey);
|
||||
await storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString);
|
||||
}
|
||||
|
||||
// Post migration tasks
|
||||
await cryptoService.ToggleKeyAsync();
|
||||
await storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow.AddYears(-1));
|
||||
await lockService.CheckLockAsync();
|
||||
|
||||
// Remove "needs migration" flag
|
||||
settingsShim.Remove(Constants.OldUserIdKey);
|
||||
await storageService.SaveAsync(Constants.MigratedFromV1, true);
|
||||
Migrating = false;
|
||||
messagingService.Send("migrated");
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
var task = Task.Run(() => syncService.FullSyncAsync(true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class CipherString
|
||||
{
|
||||
private string _decryptedValue;
|
||||
|
||||
public CipherString(string encryptedString)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(encryptedString))
|
||||
{
|
||||
throw new ArgumentException(nameof(encryptedString));
|
||||
}
|
||||
|
||||
var headerPieces = encryptedString.Split('.');
|
||||
string[] encPieces;
|
||||
|
||||
EncryptionType encType;
|
||||
if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out encType))
|
||||
{
|
||||
EncryptionType = encType;
|
||||
encPieces = headerPieces[1].Split('|');
|
||||
}
|
||||
else if(headerPieces.Length == 1)
|
||||
{
|
||||
encPieces = headerPieces[0].Split('|');
|
||||
EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 :
|
||||
EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Malformed header.");
|
||||
}
|
||||
|
||||
switch(EncryptionType)
|
||||
{
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
break;
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if(encPieces.Length != 3)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
Mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if(encPieces.Length != 1)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
Mac = encPieces[1];
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown encType.");
|
||||
}
|
||||
|
||||
EncryptedString = encryptedString;
|
||||
}
|
||||
|
||||
public CipherString(EncryptionType encryptionType, string initializationVector, string cipherText,
|
||||
string mac = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(initializationVector))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initializationVector));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cipherText));
|
||||
}
|
||||
|
||||
EncryptionType = encryptionType;
|
||||
EncryptedString = string.Format("{0}.{1}|{2}", (byte)EncryptionType, initializationVector, cipherText);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(mac))
|
||||
{
|
||||
EncryptedString = string.Format("{0}|{1}", EncryptedString, mac);
|
||||
}
|
||||
|
||||
CipherText = cipherText;
|
||||
InitializationVector = initializationVector;
|
||||
Mac = mac;
|
||||
}
|
||||
|
||||
public EncryptionType EncryptionType { get; private set; }
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector { get; private set; }
|
||||
public string CipherText { get; private set; }
|
||||
public string Mac { get; private set; }
|
||||
public byte[] InitializationVectorBytes => string.IsNullOrWhiteSpace(InitializationVector) ?
|
||||
null : Convert.FromBase64String(InitializationVector);
|
||||
public byte[] CipherTextBytes => Convert.FromBase64String(CipherText);
|
||||
public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class SymmetricCryptoKey
|
||||
{
|
||||
public SymmetricCryptoKey(byte[] rawBytes, EncryptionType? encType = null)
|
||||
{
|
||||
if(rawBytes == null || rawBytes.Length == 0)
|
||||
{
|
||||
throw new Exception("Must provide keyBytes.");
|
||||
}
|
||||
|
||||
if(encType == null)
|
||||
{
|
||||
if(rawBytes.Length == 32)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else if(rawBytes.Length == 64)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionType = encType.Value;
|
||||
Key = rawBytes;
|
||||
|
||||
if(EncryptionType == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key;
|
||||
MacKey = null;
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key.Take(16).ToArray();
|
||||
MacKey = Key.Skip(16).Take(16).ToArray();
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
{
|
||||
EncKey = Key.Take(32).ToArray();
|
||||
MacKey = Key.Skip(32).Take(32).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Key { get; set; }
|
||||
public string B64Key => Convert.ToBase64String(Key);
|
||||
public byte[] EncKey { get; set; }
|
||||
public byte[] MacKey { get; set; }
|
||||
public EncryptionType EncryptionType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public class SettingsShim
|
||||
{
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.ContainsKey(key);
|
||||
}
|
||||
|
||||
public string GetValueOrDefault(string key, string defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public DateTime GetValueOrDefault(string key, DateTime defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public bool GetValueOrDefault(string key, bool defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public int GetValueOrDefault(string key, int defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public long GetValueOrDefault(string key, long defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, string value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, DateTime value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, bool value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, long value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, int value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/App/Models/PreviousPageInfo.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class PreviousPageInfo
|
||||
{
|
||||
public string Page { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n SelfHostedEnvironment}"
|
||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
@@ -42,7 +42,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomEnvironment}"
|
||||
<Label Text="{u:I18n CustomEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class EnvironmentPage : BaseContentPage
|
||||
{
|
||||
private EnvironmentPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly EnvironmentPageViewModel _vm;
|
||||
|
||||
public EnvironmentPage()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as EnvironmentPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -33,10 +38,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
<controls:FaButton Text=""
|
||||
StyleClass="btn-muted, btn-icon, btn-icon-platform"
|
||||
HorizontalOptions="Start"
|
||||
Clicked="Settings_Clicked" />
|
||||
Clicked="Settings_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}">
|
||||
<controls:FaButton.Margin>
|
||||
<OnPlatform x:TypeArguments="Thickness">
|
||||
<On Platform="iOS" Value="0, 10, 0, 0" />
|
||||
<On Platform="Android" Value="0" />
|
||||
</OnPlatform>
|
||||
</controls:FaButton.Margin>
|
||||
</controls:FaButton>
|
||||
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
|
||||
<Image
|
||||
x:Name="_logo"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
@@ -7,12 +9,14 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HomePage : BaseContentPage
|
||||
{
|
||||
private IMessagingService _messagingService;
|
||||
|
||||
public HomePage()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
InitializeComponent();
|
||||
var theme = ThemeManager.GetTheme();
|
||||
var darkbasedTheme = theme == "dark" || theme == "black" || theme == "nord";
|
||||
_logo.Source = darkbasedTheme ? "logo_white.png" : "logo.png";
|
||||
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
public async Task DismissRegisterPageAndLogInAsync(string email)
|
||||
@@ -21,6 +25,12 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(new LoginPage(email)));
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
}
|
||||
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
Text="{Binding Pin}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -56,7 +58,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -76,6 +80,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -87,7 +93,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{Binding LockedVerifyText}"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
@@ -7,6 +10,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LockPage : BaseContentPage
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly bool _autoPromptFingerprint;
|
||||
private readonly LockPageViewModel _vm;
|
||||
@@ -16,28 +20,13 @@ namespace Bit.App.Pages
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptFingerprint = true)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_appOptions = appOptions;
|
||||
_autoPromptFingerprint = autoPromptFingerprint;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () =>
|
||||
{
|
||||
if(_appOptions != null)
|
||||
{
|
||||
if(_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
else if(_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions);
|
||||
};
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
PinEntry = _pin;
|
||||
}
|
||||
@@ -87,10 +76,7 @@ namespace Bit.App.Pages
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
Device.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -110,5 +96,28 @@ namespace Bit.App.Pages
|
||||
await _vm.PromptFingerprintAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
if(_appOptions != null)
|
||||
{
|
||||
if(_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
else if(_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if(previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _hasKey;
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
@@ -46,6 +46,7 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -102,8 +103,7 @@ namespace Bit.App.Pages
|
||||
public async Task InitAsync(bool autoPromptFingerprint)
|
||||
{
|
||||
_pinSet = await _lockService.IsPinLockSetAsync();
|
||||
_hasKey = await _cryptoService.HasKeyAsync();
|
||||
PinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2;
|
||||
PinLock = (_pinSet.Item1 && _lockService.PinProtectedKey != null) || _pinSet.Item2;
|
||||
FingerprintLock = await _lockService.IsFingerprintLockSetAsync();
|
||||
_email = await _userService.GetEmailAsync();
|
||||
var webVault = _environmentService.GetWebVaultUrl();
|
||||
@@ -126,8 +126,19 @@ namespace Bit.App.Pages
|
||||
|
||||
if(FingerprintLock)
|
||||
{
|
||||
FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
if(Device.RuntimePlatform == Device.iOS && supportsFace)
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFaceIDToUnlock;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseBiometricsToUnlock;
|
||||
}
|
||||
else
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
if(autoPromptFingerprint)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
@@ -167,14 +178,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000),
|
||||
_lockService.PinProtectedKey);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin));
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
failed = decPin != Pin;
|
||||
_lockService.PinLocked = failed;
|
||||
if(!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
DoContinue();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -219,6 +233,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
_lockService.PinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
@@ -254,7 +277,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
||||
{
|
||||
var page = Page as LockPage;
|
||||
@@ -270,21 +293,25 @@ namespace Bit.App.Pages
|
||||
_lockService.FingerprintLocked = !success;
|
||||
if(success)
|
||||
{
|
||||
DoContinue();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
if(!_hasKey)
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
if(!hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
}
|
||||
DoContinue();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
|
||||
private void DoContinue()
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_lockService.FingerprintLocked = false;
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
_messagingService.Send("unlocked");
|
||||
UnlockedAction?.Invoke();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -66,7 +68,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPage : BaseContentPage
|
||||
{
|
||||
private LoginPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly LoginPageViewModel _vm;
|
||||
|
||||
public LoginPage(string email = null)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -55,10 +60,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -18,6 +19,7 @@ namespace Bit.App.Pages
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _showPassword;
|
||||
private string _email;
|
||||
@@ -30,6 +32,7 @@ namespace Bit.App.Pages
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -123,6 +126,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
Application.Current.MainPage = new TabsPage();
|
||||
}
|
||||
@@ -130,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -64,7 +66,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
@@ -89,6 +93,8 @@
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -98,7 +104,9 @@
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class RegisterPage : BaseContentPage
|
||||
{
|
||||
private RegisterPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly RegisterPageViewModel _vm;
|
||||
|
||||
public RegisterPage(HomePage homePage)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RegisterPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -51,10 +56,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,20 @@
|
||||
<pages:TwoFactorPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_cancelItem" />
|
||||
<ToolbarItem Text="{u:I18n Continue}" Clicked="Continue_Clicked" Order="Primary"
|
||||
x:Name="_continueItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNullConverter x:Key="isNull" />
|
||||
<ToolbarItem Text="{u:I18n Continue}" Clicked="Continue_Clicked"
|
||||
x:Name="_continueItem" x:Key="continueItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="10" Padding="0, 0, 0, 10" VerticalOptions="FillAndExpand">
|
||||
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding TotpMethod, Mode=OneWay}">
|
||||
@@ -75,6 +76,9 @@
|
||||
x:Name="_yubikeyTokenEntry"
|
||||
Text="{Binding Token}"
|
||||
StyleClass="box-value"
|
||||
IsPassword="True"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
@@ -123,6 +127,10 @@
|
||||
IsVisible="{Binding EmailMethod}"
|
||||
Clicked="ResendEmail_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n TryAgain}"
|
||||
IsVisible="{Binding ShowTryAgain}"
|
||||
Clicked="TryAgain_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Bit.App.Pages
|
||||
DuoWebView = _duoWebView;
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
ToolbarItems.Remove(_cancelItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public void AddContinueButton()
|
||||
{
|
||||
if(ToolbarItems.Count == 0)
|
||||
if(!ToolbarItems.Contains(_continueItem))
|
||||
{
|
||||
ToolbarItems.Add(_continueItem);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public void RemoveContinueButton()
|
||||
{
|
||||
if(ToolbarItems.Count > 0)
|
||||
if(ToolbarItems.Contains(_continueItem))
|
||||
{
|
||||
ToolbarItems.Remove(_continueItem);
|
||||
}
|
||||
@@ -55,11 +55,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(message.Command == "gotYubiKeyOTP")
|
||||
{
|
||||
if(_vm.YubikeyMethod)
|
||||
var token = (string)message.Data;
|
||||
if(_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) &&
|
||||
token.Length == 44 && !token.Contains(" "))
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
_vm.Token = (string)message.Data;
|
||||
_vm.Token = token;
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
}
|
||||
@@ -138,5 +140,16 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAgain_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
if(_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -23,6 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _u2fSupported = false;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
@@ -40,6 +42,7 @@ namespace Bit.App.Pages
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
@@ -66,6 +69,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
|
||||
|
||||
public bool ShowTryAgain => YubikeyMethod && Device.RuntimePlatform == Device.iOS;
|
||||
|
||||
public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos :
|
||||
AppResources.YubiKeyInstruction;
|
||||
|
||||
@@ -79,6 +84,7 @@ namespace Bit.App.Pages
|
||||
nameof(YubikeyMethod),
|
||||
nameof(AuthenticatorMethod),
|
||||
nameof(TotpMethod),
|
||||
nameof(ShowTryAgain),
|
||||
});
|
||||
}
|
||||
public Command SubmitCommand { get; }
|
||||
@@ -132,10 +138,11 @@ namespace Bit.App.Pages
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(async sig =>
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
{
|
||||
Token = sig;
|
||||
await SubmitAsync();
|
||||
App.WaitForResume();
|
||||
Device.BeginInvokeOnMainThread(async () => await SubmitAsync());
|
||||
});
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
@@ -157,7 +164,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
}
|
||||
if(DuoMethod)
|
||||
if(SelectedProviderType == null || DuoMethod)
|
||||
{
|
||||
page.RemoveContinueButton();
|
||||
}
|
||||
@@ -200,13 +207,18 @@ namespace Bit.App.Pages
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
Application.Current.MainPage = new TabsPage();
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -11,8 +13,16 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private IStorageService _storageService;
|
||||
|
||||
protected int AndroidShowModalAnimationDelay = 400;
|
||||
protected int AndroidShowPageAnimationDelay = 100;
|
||||
protected int ShowModalAnimationDelay = 400;
|
||||
protected int ShowPageAnimationDelay = 100;
|
||||
|
||||
public BaseContentPage()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.FullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastPageAction { get; set; }
|
||||
|
||||
@@ -77,21 +87,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(fromModal ? AndroidShowModalAnimationDelay : AndroidShowPageAnimationDelay);
|
||||
await Task.Delay(fromModal ? ShowModalAnimationDelay : ShowPageAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(async () => await DoWorkAsync());
|
||||
});
|
||||
}
|
||||
|
||||
protected void RequestFocus(InputView input)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
input.Focus();
|
||||
return;
|
||||
}
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(AndroidShowModalAnimationDelay);
|
||||
await Task.Delay(ShowModalAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(() => input.Focus());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,17 +19,23 @@
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:DateTimeConverter x:Key="dateTime" />
|
||||
<u:ColoredPasswordConverter x:Key="coloredPassword" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Clear}"
|
||||
Clicked="Clear_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_clearItem"
|
||||
x:Key="clearItem" />
|
||||
<ToolbarItem Icon="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
Clicked="More_Clicked"
|
||||
x:Name="_moreItem"
|
||||
x:Key="moreItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Clear}"
|
||||
Clicked="Clear_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_clearItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<StackLayout x:Name="_mainLayout">
|
||||
<Label IsVisible="{Binding ShowNoData}"
|
||||
Text="{u:I18n NoPasswordsToList}"
|
||||
@@ -66,7 +72,8 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Password, Mode=OneWay}" />
|
||||
TextType="Html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
@@ -79,7 +86,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -12,6 +14,15 @@ namespace Bit.App.Pages
|
||||
SetActivityIndicator();
|
||||
_vm = BindingContext as GeneratorHistoryPageViewModel;
|
||||
_vm.Page = this;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_clearItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
@@ -34,5 +45,19 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
null, AppResources.Clear);
|
||||
if(selection == AppResources.Clear)
|
||||
{
|
||||
await _vm.ClearAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,30 +12,63 @@
|
||||
<pages:GeneratorPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
Clicked="Select_Clicked"
|
||||
Order="Primary"
|
||||
x:Name="_selectItem" />
|
||||
<ToolbarItem Text="{u:I18n PasswordHistory}"
|
||||
Clicked="History_Clicked"
|
||||
Order="Secondary" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
Clicked="Select_Clicked"
|
||||
Order="Primary"
|
||||
x:Name="_selectItem"
|
||||
x:Key="selectItem" />
|
||||
<ToolbarItem Text="{u:I18n PasswordHistory}"
|
||||
Clicked="History_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_historyItem"
|
||||
x:Key="historyItem" />
|
||||
<ToolbarItem Icon="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
Clicked="More_Clicked"
|
||||
x:Name="_moreItem"
|
||||
x:Key="moreItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
Margin="0, 12, 0, 0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Password}"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
TextType="Html"
|
||||
Margin="0, 20"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
LineBreakMode="CharacterWrap" />
|
||||
<Button Text="{u:I18n RegeneratePassword}"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
@@ -46,7 +79,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Options}"
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -54,6 +87,7 @@
|
||||
Text="{u:I18n Type}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
@@ -87,8 +121,35 @@
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n Capitalize}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Capitalize}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n IncludeNumber}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumber}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||
<StackLayout StyleClass="box-row, box-row-slider">
|
||||
@@ -119,6 +180,8 @@
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Uppercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseUppercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
@@ -130,6 +193,8 @@
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Lowercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseLowercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
@@ -141,6 +206,8 @@
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Number}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseNumbers,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
@@ -152,6 +219,8 @@
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Special}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseSpecial,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -19,9 +22,29 @@ namespace Bit.App.Pages
|
||||
_vm.Page = this;
|
||||
_fromTabPage = fromTabPage;
|
||||
_selectAction = selectAction;
|
||||
if(selectAction == null)
|
||||
var isIos = Device.RuntimePlatform == Device.iOS;
|
||||
if(selectAction != null)
|
||||
{
|
||||
ToolbarItems.Remove(_selectItem);
|
||||
if(isIos)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
}
|
||||
ToolbarItems.Add(_selectItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(isIos)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_historyItem);
|
||||
}
|
||||
}
|
||||
if(isIos)
|
||||
{
|
||||
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +82,21 @@ namespace Bit.App.Pages
|
||||
await _vm.CopyAsync();
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
null, AppResources.PasswordHistory);
|
||||
if(selection == AppResources.PasswordHistory)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private void Select_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_selectAction?.Invoke(_vm.Password);
|
||||
@@ -67,12 +105,20 @@ namespace Bit.App.Pages
|
||||
private async void History_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
|
||||
private async void LengthSlider_DragCompleted(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.SliderChangedAsync();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -17,6 +15,7 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private PasswordGenerationOptions _options;
|
||||
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
|
||||
private string _password;
|
||||
private bool _isPassword;
|
||||
private bool _uppercase;
|
||||
@@ -29,6 +28,8 @@ namespace Bit.App.Pages
|
||||
private int _length = 5;
|
||||
private int _numWords = 3;
|
||||
private string _wordSeparator;
|
||||
private bool _capitalize;
|
||||
private bool _includeNumber;
|
||||
private int _typeSelectedIndex;
|
||||
private bool _doneIniting;
|
||||
|
||||
@@ -53,7 +54,7 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Password);
|
||||
public string ColoredPassword => PasswordFormatter.FormatPassword(Password);
|
||||
|
||||
public bool IsPassword
|
||||
{
|
||||
@@ -196,6 +197,44 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public bool Capitalize
|
||||
{
|
||||
get => _capitalize;
|
||||
set
|
||||
{
|
||||
if(SetProperty(ref _capitalize, value))
|
||||
{
|
||||
_options.Capitalize = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IncludeNumber
|
||||
{
|
||||
get => _includeNumber;
|
||||
set
|
||||
{
|
||||
if(SetProperty(ref _includeNumber, value))
|
||||
{
|
||||
_options.Number = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
|
||||
{
|
||||
get => _enforcedPolicyOptions;
|
||||
set => SetProperty(ref _enforcedPolicyOptions, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(IsPolicyInEffect)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
|
||||
|
||||
public int TypeSelectedIndex
|
||||
{
|
||||
get => _typeSelectedIndex;
|
||||
@@ -211,7 +250,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_options = await _passwordGenerationService.GetOptionsAsync();
|
||||
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
|
||||
LoadFromOptions();
|
||||
await RegenerateAsync();
|
||||
_doneIniting = true;
|
||||
@@ -230,7 +269,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options);
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
await _passwordGenerationService.SaveOptionsAsync(_options);
|
||||
LoadFromOptions();
|
||||
if(regenerate)
|
||||
@@ -248,7 +287,7 @@ namespace Bit.App.Pages
|
||||
public async Task SliderInputAsync()
|
||||
{
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options);
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
Password = await _passwordGenerationService.GeneratePasswordAsync(_options);
|
||||
}
|
||||
|
||||
@@ -273,6 +312,8 @@ namespace Bit.App.Pages
|
||||
Uppercase = _options.Uppercase.GetValueOrDefault();
|
||||
Lowercase = _options.Lowercase.GetValueOrDefault();
|
||||
Length = _options.Length.GetValueOrDefault(5);
|
||||
Capitalize = _options.Capitalize.GetValueOrDefault();
|
||||
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
|
||||
}
|
||||
|
||||
private void SetOptions()
|
||||
@@ -288,6 +329,8 @@ namespace Bit.App.Pages
|
||||
_options.Uppercase = Uppercase;
|
||||
_options.Lowercase = Lowercase;
|
||||
_options.Length = Length;
|
||||
_options.Capitalize = Capitalize;
|
||||
_options.IncludeNumber = IncludeNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,40 @@
|
||||
VerticalOptions="Start"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<StackLayout IsVisible="{Binding Enabled, Converter={StaticResource inverseBool}}"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
<StackLayout VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="20">
|
||||
<StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand">
|
||||
Spacing="10">
|
||||
<StackLayout IsVisible="{Binding Enabled, Converter={StaticResource inverseBool}}"
|
||||
Orientation="Horizontal" HorizontalOptions="Center">
|
||||
<Label Text="{u:I18n Status}" />
|
||||
<Label Text="{u:I18n Disabled}"
|
||||
StyleClass="text-danger, text-bold" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding Enabled}"
|
||||
Orientation="Horizontal" HorizontalOptions="Center">
|
||||
<Label Text="{u:I18n Status}" />
|
||||
<Label Text="{u:I18n Enabled}"
|
||||
StyleClass="text-success, text-bold" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding Permitted, Converter={StaticResource inverseBool}}"
|
||||
Orientation="Horizontal" HorizontalOptions="Center">
|
||||
<Label Text="{u:I18n OverlayPermission}" />
|
||||
<Label Text="{u:I18n Denied}"
|
||||
StyleClass="text-danger, text-bold" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding Permitted}"
|
||||
Orientation="Horizontal" HorizontalOptions="Center">
|
||||
<Label Text="{u:I18n OverlayPermission}" />
|
||||
<Label Text="{u:I18n Granted}"
|
||||
StyleClass="text-success, text-bold" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding Enabled, Converter={StaticResource inverseBool}}"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="10">
|
||||
<Image Source="accessibility_step1.png"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 20, 0, 0"
|
||||
WidthRequest="300"
|
||||
HeightRequest="98" />
|
||||
<Label Text="{u:I18n BitwardenAutofillServiceStep1}"
|
||||
@@ -45,7 +67,7 @@
|
||||
StyleClass="text-sm" />
|
||||
<Image Source="accessibility_step2.png"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 20, 0, 0"
|
||||
Margin="0, 10, 0, 0"
|
||||
WidthRequest="300"
|
||||
HeightRequest="67" />
|
||||
<Label Text="{u:I18n BitwardenAutofillServiceStep2}"
|
||||
@@ -53,34 +75,42 @@
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-sm" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding Enabled}"
|
||||
<StackLayout IsVisible="{Binding EnabledWithoutPermission}"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="20">
|
||||
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
|
||||
<Label Text="{u:I18n Status}" />
|
||||
<Label Text="{u:I18n Enabled}"
|
||||
StyleClass="text-success, text-bold" />
|
||||
</StackLayout>
|
||||
<Image Source="accessibility_notification_icon.png"
|
||||
Spacing="10">
|
||||
<Image Source="accessibility_permission.png"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 20, 0, 0"
|
||||
WidthRequest="300"
|
||||
HeightRequest="54" />
|
||||
<Image Source="accessibility_notification.png"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 20, 0, 0"
|
||||
WidthRequest="300"
|
||||
HeightRequest="74" />
|
||||
<Label Text="{u:I18n BitwardenAutofillServiceNotification}"
|
||||
HeightRequest="142" />
|
||||
<Label Text="{u:I18n BitwardenAutofillServiceStep3}"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-sm" />
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n BitwardenAutofillServiceOpenAccessibilitySettings}"
|
||||
<StackLayout IsVisible="{Binding EnabledAndPermitted}"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Spacing="10">
|
||||
<Image Source="accessibility_overlay.png"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="300"
|
||||
HeightRequest="172" />
|
||||
<Label Text="{u:I18n BitwardenAutofillServiceOverlay}"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-sm" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="10">
|
||||
<Button Text="{u:I18n BitwardenAutofillServiceOpenAccessibilitySettings}"
|
||||
Clicked="Settings_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"></Button>
|
||||
<Button Text="{u:I18n BitwardenAutofillServiceOpenOverlayPermissionSettings}"
|
||||
Clicked="OverlayPermissionSettings_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_vm.UpdateEnabled();
|
||||
_vm.UpdatePermitted();
|
||||
_timerStarted = DateTime.UtcNow;
|
||||
Device.StartTimer(new TimeSpan(0, 0, 3), () =>
|
||||
{
|
||||
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
_vm.UpdateEnabled();
|
||||
_vm.UpdatePermitted();
|
||||
return true;
|
||||
});
|
||||
base.OnAppearing();
|
||||
@@ -48,5 +50,13 @@ namespace Bit.App.Pages
|
||||
_vm.OpenSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void OverlayPermissionSettings_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_vm.OpenOverlayPermissionSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Bit.App.Pages
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
private bool _enabled;
|
||||
private bool _permitted;
|
||||
|
||||
public AccessibilityServicePageViewModel()
|
||||
{
|
||||
@@ -19,17 +20,47 @@ namespace Bit.App.Pages
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set => SetProperty(ref _enabled, value);
|
||||
set => SetProperty(ref _enabled, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(EnabledWithoutPermission),
|
||||
nameof(EnabledAndPermitted)
|
||||
});
|
||||
}
|
||||
|
||||
public bool Permitted
|
||||
{
|
||||
get => _permitted;
|
||||
set => SetProperty(ref _permitted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(EnabledWithoutPermission),
|
||||
nameof(EnabledAndPermitted)
|
||||
});
|
||||
}
|
||||
|
||||
public bool EnabledWithoutPermission => Enabled && !Permitted;
|
||||
|
||||
public bool EnabledAndPermitted => Enabled && Permitted;
|
||||
|
||||
public void OpenSettings()
|
||||
{
|
||||
_deviceActionService.OpenAccessibilitySettings();
|
||||
}
|
||||
|
||||
public void OpenOverlayPermissionSettings()
|
||||
{
|
||||
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
|
||||
}
|
||||
|
||||
public void UpdateEnabled()
|
||||
{
|
||||
Enabled = _deviceActionService.AutofillAccessibilityServiceRunning();
|
||||
}
|
||||
|
||||
public void UpdatePermitted()
|
||||
{
|
||||
Permitted = _deviceActionService.AutofillAccessibilityOverlayPermitted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
src/App/Pages/Settings/AutofillPage.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AutofillPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
Title="{u:I18n PasswordAutofill}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="5"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-lg"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn1}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn2}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn3}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn4}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn5}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="autofill-kb.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
20
src/App/Pages/Settings/AutofillPage.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillPage : BaseContentPage
|
||||
{
|
||||
public AutofillPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/App/Pages/Settings/ExportVaultPage.xaml
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ExportVaultPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:ExportVaultPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ExportVaultPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:UpperCaseConverter x:Key="toUpper" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n FileFormat}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_fileFormatPicker"
|
||||
ItemsSource="{Binding FileFormatOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding FileFormatSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ExportVaultCommand}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n ExportVaultMasterPasswordDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
<Label
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
Margin="0, 20">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span
|
||||
Text="{Binding Converter={StaticResource toUpper}, ConverterParameter={u:I18n Warning}}"
|
||||
FontAttributes="Bold" />
|
||||
<Span Text=": " FontAttributes="Bold" />
|
||||
<Span Text="{u:I18n ExportVaultWarning}" />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
<StackLayout Spacing="20">
|
||||
<Button Text="{u:I18n ExportVault}"
|
||||
Clicked="ExportVault_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
69
src/App/Pages/Settings/ExportVaultPage.xaml.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ExportVaultPage : BaseContentPage
|
||||
{
|
||||
private readonly ExportVaultPageViewModel _vm;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
public ExportVaultPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_vm = BindingContext as ExportVaultPageViewModel;
|
||||
_vm.Page = this;
|
||||
_fileFormatPicker.ItemDisplayBinding = new Binding("Value");
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
_broadcasterService.Subscribe(nameof(ExportVaultPage), (message) =>
|
||||
{
|
||||
if(message.Command == "selectSaveFileResult")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var data = message.Data as Tuple<string, string>;
|
||||
if(data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_vm.SaveFileSelected(data.Item1, data.Item2);
|
||||
});
|
||||
}
|
||||
});
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
protected async override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(ExportVaultPage));
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExportVault_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await _vm.ExportVaultAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/App/Pages/Settings/ExportVaultPageViewModel.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ExportVaultPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IExportService _exportService;
|
||||
|
||||
private int _fileFormatSelectedIndex;
|
||||
private bool _showPassword;
|
||||
private string _masterPassword;
|
||||
private byte[] _exportResult;
|
||||
private string _defaultFilename;
|
||||
|
||||
public ExportVaultPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
||||
|
||||
PageTitle = AppResources.ExportVault;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ExportVaultCommand = new Command(async () => await ExportVaultAsync());
|
||||
|
||||
FileFormatOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("json", ".json"),
|
||||
new KeyValuePair<string, string>("csv", ".csv")
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, string>> FileFormatOptions { get; set; }
|
||||
|
||||
public int FileFormatSelectedIndex
|
||||
{
|
||||
get => _fileFormatSelectedIndex;
|
||||
set { SetProperty(ref _fileFormatSelectedIndex, value); }
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[] {nameof(ShowPasswordIcon)});
|
||||
}
|
||||
|
||||
public string MasterPassword
|
||||
{
|
||||
get => _masterPassword;
|
||||
set => SetProperty(ref _masterPassword, value);
|
||||
}
|
||||
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as ExportVaultPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public Command ExportVaultCommand { get; }
|
||||
|
||||
public async Task ExportVaultAsync()
|
||||
{
|
||||
if(string.IsNullOrEmpty(_masterPassword))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
|
||||
return;
|
||||
}
|
||||
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(_masterPassword, null);
|
||||
MasterPassword = string.Empty;
|
||||
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = _exportService.GetExport(FileFormatOptions[FileFormatSelectedIndex].Key);
|
||||
var fileFormat = FileFormatOptions[FileFormatSelectedIndex].Key;
|
||||
_defaultFilename = _exportService.GetFileName(null, fileFormat);
|
||||
_exportResult = Encoding.ASCII.GetBytes(data.Result);
|
||||
|
||||
if(!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
|
||||
{
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
|
||||
}
|
||||
}
|
||||
|
||||
public async void SaveFileSelected(string contentUri, string filename)
|
||||
{
|
||||
if(_deviceActionService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
|
||||
{
|
||||
ClearResult();
|
||||
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
|
||||
return;
|
||||
}
|
||||
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
}
|
||||
|
||||
private void ClearResult()
|
||||
{
|
||||
_defaultFilename = null;
|
||||
_exportResult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/App/Pages/Settings/ExtensionPage.xaml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ExtensionPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:ExtensionPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ExtensionPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Padding="0" Spacing="0" VerticalOptions="FillAndExpand">
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding NotStarted}">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-more.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndNotActivated}">
|
||||
<Label Text="{u:I18n ExtensionAlmostDone}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTapIcon}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-act.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndActivated}">
|
||||
<Label Text="{u:I18n ExtensionReady}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionInSafari}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-use.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExntesionReenable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
38
src/App/Pages/Settings/ExtensionPage.xaml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ExtensionPage : BaseContentPage
|
||||
{
|
||||
private readonly ExtensionPageViewModel _vm;
|
||||
|
||||
public ExtensionPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as ExtensionPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private void Show_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_vm.ShowExtension();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/App/Pages/Settings/ExtensionPageViewModel.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ExtensionPageViewModel : BaseViewModel
|
||||
{
|
||||
private const string StartedKey = "appExtensionStarted";
|
||||
private const string ActivatedKey = "appExtensionActivated";
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _started;
|
||||
private bool _activated;
|
||||
|
||||
public ExtensionPageViewModel()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
PageTitle = AppResources.AppExtension;
|
||||
}
|
||||
|
||||
public bool Started
|
||||
{
|
||||
get => _started;
|
||||
set => SetProperty(ref _started, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(NotStarted),
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool Activated
|
||||
{
|
||||
get => _activated;
|
||||
set => SetProperty(ref _activated, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool NotStarted => !Started;
|
||||
public bool StartedAndNotActivated => Started && !Activated;
|
||||
public bool StartedAndActivated => Started && Activated;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var started = await _storageService.GetAsync<bool?>(StartedKey);
|
||||
var activated = await _storageService.GetAsync<bool?>(ActivatedKey);
|
||||
Started = started.GetValueOrDefault();
|
||||
Activated = activated.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public void ShowExtension()
|
||||
{
|
||||
_messagingService.Send("showAppExtension", this);
|
||||
}
|
||||
|
||||
public void EnabledExtension(bool enabled)
|
||||
{
|
||||
Started = true;
|
||||
if(!Activated && enabled)
|
||||
{
|
||||
Activated = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,15 @@
|
||||
IsDestructive="True"
|
||||
x:Name="_deleteItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Xamarin.Forms;
|
||||
using Bit.App.Resources;
|
||||
using System.Collections.Generic;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -19,6 +21,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.Remove(_deleteItem);
|
||||
}
|
||||
if(_vm.EditMode && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
@@ -61,5 +67,20 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if(!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new List<string> { };
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||
if(selection == AppResources.Delete)
|
||||
{
|
||||
await _vm.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ namespace Bit.App.Pages
|
||||
if(EditMode)
|
||||
{
|
||||
var folder = await _folderService.GetAsync(FolderId);
|
||||
Folder = await folder.DecryptAsync();
|
||||
if(folder != null)
|
||||
{
|
||||
Folder = await folder.DecryptAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,6 +60,10 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if(Folder == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
@@ -86,13 +93,21 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync()
|
||||
{
|
||||
if(Folder == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
@@ -117,7 +132,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
<pages:FoldersPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem x:Name="_closeItem" x:Key="closeItem" Text="{u:I18n Close}"
|
||||
Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem x:Name="_addItem" x:Key="addItem" Icon="plus.png"
|
||||
Clicked="AddButton_Clicked" Order="Primary"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n AddItem}" />
|
||||
<StackLayout x:Name="_mainLayout" x:Key="mainLayout">
|
||||
<Label IsVisible="{Binding ShowNoData}"
|
||||
Text="{u:I18n NoFoldersToList}"
|
||||
@@ -68,7 +70,9 @@
|
||||
x:Name="_fab"
|
||||
ImageName="plus.png"
|
||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize">
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n AddFolder}">
|
||||
</fab:FloatingActionButtonView>
|
||||
</AbsoluteLayout>
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ namespace Bit.App.Pages
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Add(_closeItem);
|
||||
ToolbarItems.Add(_addItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
_fab.Clicked = AddButton_Clicked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class FoldersPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
private bool _showNoData;
|
||||
|
||||
public FoldersPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
|
||||
PageTitle = AppResources.Folders;
|
||||
|
||||