diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3fa0d6a09 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +# All files +[*] +guidelines = 120 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..1ff0c4230 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index fe49c72d5..781652554 100644 --- a/.gitignore +++ b/.gitignore @@ -197,4 +197,5 @@ FakesAssemblies/ # Other project.lock.json -.DS_Store \ No newline at end of file +.DS_Store +src/App/Css \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 0841a3c86..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,29 +0,0 @@ -#init: -# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -#on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -#install: -# - choco install cloc --no-progress -# - "cloc --vcs git --exclude-dir Resources,store,test,UWP,Properties --include-lang C#,JavaScript,TypeScript,PowerShell" -# - appveyor DownloadFile https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -# - appveyor DownloadFile https://aka.ms/vs/15/release/vs_community.exe -# - vs_community.exe update --wait --quiet --norestart --installPath "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community" -# - ps: .\src\Android\update-android.ps1 -before_build: - - nuget restore - - IF DEFINED keystore_dec_secret nuget install secure-file -ExcludeVersion - - IF DEFINED google_services_dec_secret secure-file\tools\secure-file -decrypt src\Android\google-services.json.enc -secret %google_services_dec_secret% -after_build: - - ps: IF($env:keystore_dec_secret) { .\src\Android\ci-build-apks.ps1 } -on_success: - - IF DEFINED play_dec_secret secure-file\tools\secure-file -decrypt store\google\Publisher\play_creds.json.enc -secret %play_dec_secret% - - IF DEFINED play_dec_secret dotnet store\google\Publisher\bin\Release\netcoreapp2.0\Publisher.dll %APPVEYOR_BUILD_FOLDER%\store\google\Publisher\play_creds.json %APPVEYOR_BUILD_FOLDER%\com.x8bit.bitwarden-%APPVEYOR_BUILD_NUMBER%.apk alpha -artifacts: - - path: com.x8bit.bitwarden-%APPVEYOR_BUILD_NUMBER%.apk - - path: com.x8bit.bitwarden-fdroid-%APPVEYOR_BUILD_NUMBER%.apk -branches: - except: - - l10n_master -skip_tags: true -configuration: Release -image: Visual Studio 2017 diff --git a/bitwarden-mobile.sln b/bitwarden-mobile.sln index 417cf1c94..5691f2367 100644 --- a/bitwarden-mobile.sln +++ b/bitwarden-mobile.sln @@ -1,523 +1,233 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +VisualStudioVersion = 15.0.28307.539 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{04B18ED2-B76D-4947-8474-191F8FD2B5E0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS", "src\iOS\iOS.csproj", "{1F78403F-9A28-405B-9289-B9DBEB55F074}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS", "src\iOS\iOS.csproj", "{599E0201-420A-4C3E-A7BA-5349F72E0B15}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0D790714-ECF8-4A83-BE4A-E9C84DD1BB5D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "src\App\App.csproj", "{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EC730FD9-F623-4B6C-B503-95CDCFBCF277}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{4B8A8C41-9820-4341-974C-41E65B7F4366}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Test", "test\App.Test\App.Test.csproj", "{A300DCE1-8D10-4267-B96A-CB01AEB7C220}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "test\Playground\Playground.csproj", "{9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Extension", "src\iOS.Extension\iOS.Extension.csproj", "{32F5A2D6-F54D-4DA1-AE26-0A980D48F422}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D10CA4A9-F866-40E1-B658-F69051236C71}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Core", "src\iOS.Core\iOS.Core.csproj", "{B2538ADA-B605-4D6F-ACD2-62A409680F84}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8904C536-C67D-420F-9971-51B26574C3AA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "store", "store", "{92470CBD-9047-4C3C-8EA3-D972D6622D84}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "google", "google", "{2E399654-26A2-46F6-B9CA-1B496A3F370A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "src\App\App.csproj", "{8A279EE4-4537-4656-9C93-44945E594556}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Publisher", "store\google\Publisher\Publisher.csproj", "{D5D91152-CB01-4F24-A503-304D3A94408B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F0E2E596-C3DB-474A-9C88-7824662894FA}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - appveyor.yml = appveyor.yml - crowdin.yml = crowdin.yml - README.md = README.md - SECURITY.md = SECURITY.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Autofill", "src\iOS.Autofill\iOS.Autofill.csproj", "{C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Core", "src\iOS.Core\iOS.Core.csproj", "{E71F3053-056C-4381-9638-048ED73BDFF6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU - Ad-Hoc|ARM = Ad-Hoc|ARM Ad-Hoc|iPhone = Ad-Hoc|iPhone Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator - Ad-Hoc|x64 = Ad-Hoc|x64 - Ad-Hoc|x86 = Ad-Hoc|x86 AppStore|Any CPU = AppStore|Any CPU - AppStore|ARM = AppStore|ARM AppStore|iPhone = AppStore|iPhone AppStore|iPhoneSimulator = AppStore|iPhoneSimulator - AppStore|x64 = AppStore|x64 - AppStore|x86 = AppStore|x86 Debug|Any CPU = Debug|Any CPU - Debug|ARM = Debug|ARM Debug|iPhone = Debug|iPhone Debug|iPhoneSimulator = Debug|iPhoneSimulator - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - FDroid|Any CPU = FDroid|Any CPU - FDroid|ARM = FDroid|ARM - FDroid|iPhone = FDroid|iPhone - FDroid|iPhoneSimulator = FDroid|iPhoneSimulator - FDroid|x64 = FDroid|x64 - FDroid|x86 = FDroid|x86 Release|Any CPU = Release|Any CPU - Release|ARM = Release|ARM Release|iPhone = Release|iPhone Release|iPhoneSimulator = Release|iPhoneSimulator - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|ARM.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|ARM.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x64.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x64.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Ad-Hoc|x86.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|Any CPU.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|Any CPU.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|ARM.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|ARM.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|ARM.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x64.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x64.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x64.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x86.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x86.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.AppStore|x86.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|ARM.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|ARM.Build.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|ARM.Deploy.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|x64.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|x86.ActiveCfg = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|x86.Build.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Debug|x86.Deploy.0 = Debug|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|Any CPU.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|Any CPU.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|Any CPU.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|ARM.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|ARM.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|ARM.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhone.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhone.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhone.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhoneSimulator.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhoneSimulator.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|iPhoneSimulator.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x64.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x64.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x64.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x86.ActiveCfg = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x86.Build.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.FDroid|x86.Deploy.0 = FDroid|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|Any CPU.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|Any CPU.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|ARM.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|ARM.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|ARM.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|iPhone.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x64.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x64.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x64.Deploy.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x86.ActiveCfg = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x86.Build.0 = Release|Any CPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0}.Release|x86.Deploy.0 = Release|Any CPU - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|ARM.ActiveCfg = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|iPhone.ActiveCfg = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|iPhone.Build.0 = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|x64.ActiveCfg = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.AppStore|x86.ActiveCfg = AppStore|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|Any CPU.ActiveCfg = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|ARM.ActiveCfg = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|iPhone.ActiveCfg = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|iPhone.Build.0 = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|x64.ActiveCfg = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|x86.ActiveCfg = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Debug|x86.Build.0 = Debug|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|Any CPU.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|Any CPU.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|ARM.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|ARM.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|iPhone.ActiveCfg = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|iPhone.Build.0 = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|x64.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|x64.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|x86.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.FDroid|x86.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|Any CPU.ActiveCfg = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|ARM.ActiveCfg = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|iPhone.ActiveCfg = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|iPhone.Build.0 = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|x64.ActiveCfg = Release|iPhone - {1F78403F-9A28-405B-9289-B9DBEB55F074}.Release|x86.ActiveCfg = Release|iPhone - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|ARM.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|x64.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|Any CPU.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|ARM.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|ARM.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|x64.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|x64.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|x86.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.AppStore|x86.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|ARM.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|ARM.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|x64.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|x86.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Debug|x86.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|Any CPU.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|Any CPU.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|ARM.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|ARM.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|iPhone.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|iPhone.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|iPhoneSimulator.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|x64.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|x64.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|x86.ActiveCfg = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.FDroid|x86.Build.0 = Debug|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|Any CPU.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|ARM.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|ARM.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|iPhone.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|x64.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|x64.Build.0 = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|x86.ActiveCfg = Release|Any CPU - {A300DCE1-8D10-4267-B96A-CB01AEB7C220}.Release|x86.Build.0 = Release|Any CPU - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|ARM.ActiveCfg = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|iPhone.ActiveCfg = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|iPhone.Build.0 = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|x64.ActiveCfg = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.AppStore|x86.ActiveCfg = AppStore|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|Any CPU.ActiveCfg = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|ARM.ActiveCfg = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|iPhone.ActiveCfg = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|iPhone.Build.0 = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|x64.ActiveCfg = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|x86.ActiveCfg = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Debug|x86.Build.0 = Debug|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|Any CPU.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|Any CPU.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|ARM.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|ARM.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|iPhone.ActiveCfg = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|iPhone.Build.0 = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|x64.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|x64.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|x86.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.FDroid|x86.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|Any CPU.ActiveCfg = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|ARM.ActiveCfg = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|iPhone.ActiveCfg = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|iPhone.Build.0 = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|x64.ActiveCfg = Release|iPhone - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422}.Release|x86.ActiveCfg = Release|iPhone - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|ARM.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|x64.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|Any CPU.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|ARM.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|ARM.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|iPhone.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|x64.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|x64.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|x86.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.AppStore|x86.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|ARM.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|iPhone.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Debug|x86.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|Any CPU.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|Any CPU.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|ARM.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|ARM.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|iPhone.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|iPhone.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|iPhoneSimulator.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|x64.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|x64.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|x86.ActiveCfg = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.FDroid|x86.Build.0 = Debug|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|ARM.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|iPhone.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|iPhone.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|x64.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|x64.Build.0 = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|x86.ActiveCfg = Release|Any CPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84}.Release|x86.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|ARM.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|iPhone.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|x64.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|x64.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|x86.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.AppStore|x86.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|ARM.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|ARM.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|iPhone.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|x64.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|x64.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|x86.ActiveCfg = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Debug|x86.Build.0 = Debug|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|Any CPU.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|Any CPU.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|ARM.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|ARM.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|iPhone.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|iPhone.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|iPhoneSimulator.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|iPhoneSimulator.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|x64.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|x64.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|x86.ActiveCfg = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.FDroid|x86.Build.0 = FDroid|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|Any CPU.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|ARM.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|ARM.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|iPhone.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|iPhone.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|x64.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|x64.Build.0 = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|x86.ActiveCfg = Release|Any CPU - {8A279EE4-4537-4656-9C93-44945E594556}.Release|x86.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|ARM.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|iPhone.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|x64.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|x64.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|x86.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.AppStore|x86.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|ARM.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|ARM.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|iPhone.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|x64.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|x64.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|x86.ActiveCfg = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Debug|x86.Build.0 = Debug|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|Any CPU.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|Any CPU.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|ARM.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|ARM.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|iPhone.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|iPhone.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|iPhoneSimulator.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|x64.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|x64.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|x86.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.FDroid|x86.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|Any CPU.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|ARM.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|ARM.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|iPhone.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|iPhone.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|x64.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|x64.Build.0 = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|x86.ActiveCfg = Release|Any CPU - {D5D91152-CB01-4F24-A503-304D3A94408B}.Release|x86.Build.0 = Release|Any CPU - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|ARM.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|iPhone.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|iPhone.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|x64.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.AppStore|x86.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|Any CPU.ActiveCfg = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|ARM.ActiveCfg = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|iPhone.ActiveCfg = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|iPhone.Build.0 = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|x64.ActiveCfg = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Debug|x86.ActiveCfg = Debug|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|Any CPU.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|Any CPU.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|ARM.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|ARM.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|iPhone.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|iPhone.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|x64.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|x64.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|x86.ActiveCfg = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.FDroid|x86.Build.0 = AppStore|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|Any CPU.ActiveCfg = Release|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|ARM.ActiveCfg = Release|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|iPhone.ActiveCfg = Release|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|iPhone.Build.0 = Release|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|x64.ActiveCfg = Release|iPhone - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F}.Release|x86.ActiveCfg = Release|iPhone + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhone.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|Any CPU.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|Any CPU.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhone.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhone.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.AppStore|iPhoneSimulator.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhone.Build.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhone.Deploy.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|Any CPU.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|Any CPU.Deploy.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhone.ActiveCfg = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhone.Build.0 = Release|Any CPU + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhone.Deploy.0 = Release|Any CPU + {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 + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|Any CPU.Deploy.0 = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhone.Deploy.0 = Ad-Hoc|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Ad-Hoc|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|Any CPU.Build.0 = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|Any CPU.Deploy.0 = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhone.ActiveCfg = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhone.Build.0 = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhone.Deploy.0 = AppStore|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.AppStore|iPhoneSimulator.Deploy.0 = AppStore|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|Any CPU.Deploy.0 = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhone.ActiveCfg = Debug|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhone.Build.0 = Debug|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhone.Deploy.0 = Debug|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Debug|iPhoneSimulator.Deploy.0 = Debug|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|Any CPU.ActiveCfg = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|Any CPU.Build.0 = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|Any CPU.Deploy.0 = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhone.ActiveCfg = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhone.Build.0 = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhone.Deploy.0 = Release|iPhone + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.Deploy.0 = Release|iPhoneSimulator + {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}.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 + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhone.Build.0 = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhone.Deploy.0 = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|Any CPU.Build.0 = Release|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhone.ActiveCfg = Release|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhone.Build.0 = Release|Any CPU + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhone.Deploy.0 = Release|Any CPU + {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}.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 + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|iPhone.Build.0 = Debug|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|Any CPU.Build.0 = Release|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|iPhone.ActiveCfg = Release|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|iPhone.Build.0 = Release|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4B8A8C41-9820-4341-974C-41E65B7F4366}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|iPhone.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|iPhone.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|Any CPU.Build.0 = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|iPhone.ActiveCfg = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|iPhone.Build.0 = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = Release|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 + {E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.Build.0 = Debug|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|Any CPU.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|iPhone.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|iPhone.Build.0 = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E71F3053-056C-4381-9638-048ED73BDFF6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {04B18ED2-B76D-4947-8474-191F8FD2B5E0} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} - {1F78403F-9A28-405B-9289-B9DBEB55F074} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} - {A300DCE1-8D10-4267-B96A-CB01AEB7C220} = {0D790714-ECF8-4A83-BE4A-E9C84DD1BB5D} - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} - {B2538ADA-B605-4D6F-ACD2-62A409680F84} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} - {2E399654-26A2-46F6-B9CA-1B496A3F370A} = {92470CBD-9047-4C3C-8EA3-D972D6622D84} - {8A279EE4-4537-4656-9C93-44945E594556} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} - {D5D91152-CB01-4F24-A503-304D3A94408B} = {2E399654-26A2-46F6-B9CA-1B496A3F370A} - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F} = {EC730FD9-F623-4B6C-B503-95CDCFBCF277} + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F} = {D10CA4A9-F866-40E1-B658-F69051236C71} + {599E0201-420A-4C3E-A7BA-5349F72E0B15} = {D10CA4A9-F866-40E1-B658-F69051236C71} + {EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C} = {D10CA4A9-F866-40E1-B658-F69051236C71} + {4B8A8C41-9820-4341-974C-41E65B7F4366} = {D10CA4A9-F866-40E1-B658-F69051236C71} + {9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3} = {8904C536-C67D-420F-9971-51B26574C3AA} + {E71F3053-056C-4381-9638-048ED73BDFF6} = {D10CA4A9-F866-40E1-B658-F69051236C71} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {318CB2DF-0118-43A3-AC83-56BADCF71CCD} + SolutionGuid = {7D436EA3-8B7E-45D2-8D14-0730BD2E0410} EndGlobalSection EndGlobal diff --git a/src/Android/AutofillActivity.cs b/src/Android/Accessibility/AccessibilityActivity.cs similarity index 82% rename from src/Android/AutofillActivity.cs rename to src/Android/Accessibility/AccessibilityActivity.cs index fac004836..20475184c 100644 --- a/src/Android/AutofillActivity.cs +++ b/src/Android/Accessibility/AccessibilityActivity.cs @@ -5,16 +5,14 @@ using Android.Runtime; using Android.Views; using System; -namespace Bit.Android +namespace Bit.Droid.Accessibility { - [Activity(Theme = "@style/BitwardenTheme.Splash", WindowSoftInputMode = SoftInput.StateHidden)] - public class AutofillActivity : Activity + [Activity(Theme = "@style/MainTheme.Splash", WindowSoftInputMode = SoftInput.StateHidden)] + public class AccessibilityActivity : Activity { private DateTime? _lastLaunch = null; private string _lastQueriedUri; - public static AutofillCredentials LastCredentials { get; set; } - protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); @@ -40,7 +38,6 @@ namespace Bit.Android Finish(); return; } - Intent.RemoveExtra("uri"); } @@ -49,7 +46,7 @@ namespace Bit.Android base.OnActivityResult(requestCode, resultCode, data); if(data == null) { - LastCredentials = null; + AccessibilityHelpers.LastCredentials = null; } else { @@ -57,15 +54,14 @@ namespace Bit.Android { if(data.GetStringExtra("canceled") != null) { - LastCredentials = null; + AccessibilityHelpers.LastCredentials = null; } else { var uri = data.GetStringExtra("uri"); var username = data.GetStringExtra("username"); var password = data.GetStringExtra("password"); - - LastCredentials = new AutofillCredentials + AccessibilityHelpers.LastCredentials = new Credentials { Username = username, Password = password, @@ -76,10 +72,9 @@ namespace Bit.Android } catch { - LastCredentials = null; + AccessibilityHelpers.LastCredentials = null; } } - Finish(); } @@ -91,9 +86,8 @@ namespace Bit.Android Finish(); return; } - var now = DateTime.UtcNow; - if(_lastLaunch.HasValue && (now - _lastLaunch.Value <= TimeSpan.FromSeconds(2))) + if(_lastLaunch.HasValue && (now - _lastLaunch.Value) <= TimeSpan.FromSeconds(2)) { return; } diff --git a/src/Android/Accessibility/AccessibilityHelpers.cs b/src/Android/Accessibility/AccessibilityHelpers.cs new file mode 100644 index 000000000..b9af9e05c --- /dev/null +++ b/src/Android/Accessibility/AccessibilityHelpers.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Android.OS; +using Android.Views.Accessibility; +using Bit.Core; + +namespace Bit.Droid.Accessibility +{ + public static class AccessibilityHelpers + { + public static Credentials LastCredentials = null; + public static string SystemUiPackage = "com.android.systemui"; + public static string BitwardenTag = "bw_access"; + + public static Dictionary SupportedBrowsers => new List + { + new Browser("com.android.chrome", "url_bar"), + new Browser("com.chrome.beta", "url_bar"), + new Browser("org.chromium.chrome", "url_bar"), + new Browser("com.android.browser", "url"), + new Browser("com.brave.browser", "url_bar"), + new Browser("com.opera.browser", "url_field"), + new Browser("com.opera.browser.beta", "url_field"), + new Browser("com.opera.mini.native", "url_field"), + new Browser("com.opera.touch", "addressbarEdit"), + new Browser("com.chrome.dev", "url_bar"), + new Browser("com.chrome.canary", "url_bar"), + new Browser("com.google.android.apps.chrome", "url_bar"), + new Browser("com.google.android.apps.chrome_dev", "url_bar"), + new Browser("org.codeaurora.swe.browser", "url_bar"), + 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", + (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.focus", "display_url"), + new Browser("org.mozilla.klar", "display_url"), + new Browser("org.mozilla.fenix", "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"), + new Browser("com.htc.sense.browser", "title"), + new Browser("com.amazon.cloud9", "url"), + new Browser("mobi.mgeek.TunnyBrowser", "title"), + new Browser("com.nubelacorp.javelin", "enterUrl"), + new Browser("com.jerky.browser2", "enterUrl"), + new Browser("com.mx.browser", "address_editor_with_progress"), + new Browser("com.mx.browser.tablet", "address_editor_with_progress"), + new Browser("com.linkbubble.playstore", "url_text"), + new Browser("com.ksmobile.cb", "address_bar_edit_text"), + new Browser("acr.browser.lightning", "search"), + new Browser("acr.browser.barebones", "search"), + new Browser("com.microsoft.emmx", "url_bar"), + new Browser("com.duckduckgo.mobile.android", "omnibarTextInput"), + new Browser("mark.via.gp", "aw"), + new Browser("org.bromite.bromite", "url_bar"), + new Browser("com.kiwibrowser.browser", "url_bar"), + new Browser("com.ecosia.android", "url_bar"), + new Browser("com.qwant.liberty", "url_bar_title"), + }.ToDictionary(n => n.PackageName); + + // Known packages to skip + public static HashSet FilteredPackageNames => new HashSet + { + SystemUiPackage, + "com.google.android.googlequicksearchbox", + "com.google.android.apps.nexuslauncher", + "com.google.android.launcher", + "com.computer.desktop.ui.launcher", + "com.launcher.notelauncher", + "com.anddoes.launcher", + "com.actionlauncher.playstore", + "ch.deletescape.lawnchair.plah", + "com.microsoft.launcher", + "com.teslacoilsw.launcher", + "com.teslacoilsw.launcher.prime", + "is.shortcut", + "me.craftsapp.nlauncher", + "com.ss.squarehome2" + }; + + 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 }); + } + + public static string GetUri(AccessibilityNodeInfo root) + { + var uri = string.Concat(Constants.AndroidAppProtocol, root.PackageName); + if(SupportedBrowsers.ContainsKey(root.PackageName)) + { + var browser = SupportedBrowsers[root.PackageName]; + var addressNode = root.FindAccessibilityNodeInfosByViewId( + $"{root.PackageName}:id/{browser.UriViewId}").FirstOrDefault(); + if(addressNode != null) + { + uri = ExtractUri(uri, addressNode, browser); + addressNode.Dispose(); + } + } + return uri; + } + + public static string ExtractUri(string uri, AccessibilityNodeInfo addressNode, Browser browser) + { + if(addressNode?.Text == null) + { + return uri; + } + if(addressNode.Text == null) + { + return uri; + } + uri = browser.GetUriFunction(addressNode.Text)?.Trim(); + if(uri != null && uri.Contains(".")) + { + if(!uri.Contains("://") && !uri.Contains(" ")) + { + uri = string.Concat("http://", uri); + } + else if(Build.VERSION.SdkInt <= BuildVersionCodes.KitkatWatch) + { + var parts = uri.Split(new string[] { ". " }, StringSplitOptions.None); + if(parts.Length > 1) + { + var urlPart = parts.FirstOrDefault(p => p.StartsWith("http")); + if(urlPart != null) + { + uri = urlPart.Trim(); + } + } + } + } + return uri; + } + + /// + /// Check to make sure it is ok to autofill still on the current screen + /// + public static bool NeedToAutofill(Credentials credentials, string currentUriString) + { + if(credentials == null) + { + return false; + } + if(Uri.TryCreate(credentials.LastUri, UriKind.Absolute, out Uri lastUri) && + Uri.TryCreate(currentUriString, UriKind.Absolute, out Uri currentUri)) + { + return lastUri.Host == currentUri.Host; + } + return false; + } + + public static bool EditText(AccessibilityNodeInfo n) + { + return n?.ClassName?.Contains("EditText") ?? false; + } + + + public static void FillCredentials(AccessibilityNodeInfo usernameNode, + IEnumerable passwordNodes) + { + FillEditText(usernameNode, LastCredentials?.Username); + foreach(var n in passwordNodes) + { + FillEditText(n, LastCredentials?.Password); + } + } + + public static void FillEditText(AccessibilityNodeInfo editTextNode, string value) + { + if(editTextNode == null || value == null) + { + return; + } + var bundle = new Bundle(); + bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value); + editTextNode.PerformAction(Android.Views.Accessibility.Action.SetText, bundle); + } + + public static NodeList GetWindowNodes(AccessibilityNodeInfo n, AccessibilityEvent e, + Func condition, bool disposeIfUnused, NodeList nodes = null, + int recursionDepth = 0) + { + if(nodes == null) + { + nodes = new NodeList(); + } + var dispose = disposeIfUnused; + if(n != null && recursionDepth < 50) + { + var add = n.WindowId == e.WindowId && + !(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) && + condition(n); + if(add) + { + dispose = false; + nodes.Add(n); + } + + for(var i = 0; i < n.ChildCount; i++) + { + var childNode = n.GetChild(i); + if(i > 100) + { + Android.Util.Log.Info(BitwardenTag, "Too many child iterations."); + break; + } + else if(childNode.GetHashCode() == n.GetHashCode()) + { + Android.Util.Log.Info(BitwardenTag, "Child node is the same as parent for some reason."); + } + else + { + GetWindowNodes(childNode, e, condition, true, nodes, recursionDepth++); + } + } + } + if(dispose) + { + n?.Dispose(); + } + return nodes; + } + + public static void GetNodesAndFill(AccessibilityNodeInfo root, AccessibilityEvent e, + IEnumerable passwordNodes) + { + var allEditTexts = GetWindowNodes(root, e, n => EditText(n), false); + var usernameEditText = GetUsernameEditText(allEditTexts); + FillCredentials(usernameEditText, passwordNodes); + allEditTexts.Dispose(); + usernameEditText = null; + } + + public static AccessibilityNodeInfo GetUsernameEditText(IEnumerable allEditTexts) + { + return allEditTexts.TakeWhile(n => !n.Password).LastOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/Android/Accessibility/AccessibilityService.cs b/src/Android/Accessibility/AccessibilityService.cs new file mode 100644 index 000000000..ccee0bd92 --- /dev/null +++ b/src/Android/Accessibility/AccessibilityService.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views.Accessibility; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; + +namespace Bit.Droid.Accessibility +{ + [Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")] + [IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })] + [MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")] + [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 _launcherPackageNames = null; + private DateTime? _lastLauncherSetBuilt = null; + private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1); + + public override void OnAccessibilityEvent(AccessibilityEvent e) + { + try + { + var powerManager = GetSystemService(PowerService) as PowerManager; + if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch && !powerManager.IsInteractive) + { + return; + } + else if(Build.VERSION.SdkInt < BuildVersionCodes.Lollipop && !powerManager.IsScreenOn) + { + return; + } + + if(SkipPackage(e?.PackageName)) + { + return; + } + + var root = RootInActiveWindow; + if(root == null || root.PackageName != e.PackageName) + { + return; + } + + // AccessibilityHelpers.PrintTestData(root, e); + LoadServices(); + var settingsTask = LoadSettingsAsync(); + + var notificationManager = GetSystemService(NotificationService) as NotificationManager; + var cancelNotification = true; + + switch(e.EventType) + { + case EventTypes.ViewFocused: + if(e.Source == null || !e.Source.Password || !_settingAutofillPasswordField) + { + break; + } + if(e.PackageName == BitwardenPackage) + { + CancelNotification(notificationManager); + break; + } + if(ScanAndAutofill(root, e, notificationManager, cancelNotification)) + { + CancelNotification(notificationManager); + } + break; + case EventTypes.WindowContentChanged: + case EventTypes.WindowStateChanged: + if(_settingAutofillPasswordField && e.Source.Password) + { + 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); + break; + } + + if(_settingAutofillPersistNotification) + { + 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; + } + else + { + cancelNotification = ScanAndAutofill(root, e, notificationManager, cancelNotification); + } + + if(cancelNotification) + { + CancelNotification(notificationManager); + } + break; + default: + break; + } + + notificationManager?.Dispose(); + root.Dispose(); + e.Dispose(); + } + // Suppress exceptions so that service doesn't crash. + catch { } + } + + public override void OnInterrupt() + { + // Do nothing. + } + + public bool ScanAndAutofill(AccessibilityNodeInfo root, AccessibilityEvent e, + NotificationManager notificationManager, bool cancelNotification) + { + var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e, n => n.Password, false); + if(passwordNodes.Count > 0) + { + var uri = AccessibilityHelpers.GetUri(root); + if(uri != null && !uri.Contains(BitwardenWebsite)) + { + if(AccessibilityHelpers.NeedToAutofill(AccessibilityHelpers.LastCredentials, uri)) + { + AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes); + } + else + { + NotifyToAutofill(uri, notificationManager); + cancelNotification = false; + } + } + AccessibilityHelpers.LastCredentials = null; + } + else if(AccessibilityHelpers.LastCredentials != null) + { + Task.Run(async () => + { + await Task.Delay(1000); + AccessibilityHelpers.LastCredentials = null; + }); + } + passwordNodes.Dispose(); + return cancelNotification; + } + + public void CancelNotification(NotificationManager notificationManager, long limit = 250) + { + if(Java.Lang.JavaSystem.CurrentTimeMillis() - _lastNotificationTime < limit) + { + return; + } + _lastNotificationUri = null; + notificationManager?.Cancel(AutoFillNotificationId); + } + + private void NotifyToAutofill(string uri, NotificationManager notificationManager) + { + if(notificationManager == null || string.IsNullOrWhiteSpace(uri)) + { + 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.notification_sm) + .SetContentTitle(AppResources.BitwardenAutofillService) + .SetContentText(notificationContent) + .SetTicker(notificationContent) + .SetWhen(now) + .SetContentIntent(pendingIntent); + + if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch) + { + builder.SetVisibility(NotificationVisibility.Secret) + .SetColor(Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext, + Resource.Color.primary)); + } + if(Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + if(_notificationChannel == null) + { + _notificationChannel = new NotificationChannel("bitwarden_autofill_service", + AppResources.AutofillService, NotificationImportance.Low); + notificationManager.CreateNotificationChannel(_notificationChannel); + } + builder.SetChannelId(_notificationChannel.Id); + } + if(/*Build.VERSION.SdkInt <= BuildVersionCodes.N && */_settingAutofillPersistNotification) + { + builder.SetPriority(-2); + } + + _lastNotificationTime = now; + _lastNotificationUri = uri; + notificationManager.Notify(AutoFillNotificationId, builder.Build()); + builder.Dispose(); + } + + private bool SkipPackage(string eventPackageName) + { + if(string.IsNullOrWhiteSpace(eventPackageName) || + AccessibilityHelpers.FilteredPackageNames.Contains(eventPackageName) || + eventPackageName.Contains("launcher")) + { + return true; + } + if(_launcherPackageNames == null || _lastLauncherSetBuilt == null || + (DateTime.Now - _lastLauncherSetBuilt.Value) > _rebuildLauncherSpan) + { + // refresh launcher list every now and then + _lastLauncherSetBuilt = DateTime.Now; + var intent = new Intent(Intent.ActionMain); + intent.AddCategory(Intent.CategoryHome); + var resolveInfo = PackageManager.QueryIntentActivities(intent, 0); + _launcherPackageNames = resolveInfo.Select(ri => ri.ActivityInfo.PackageName).ToHashSet(); + } + return _launcherPackageNames.Contains(eventPackageName); + } + + private void LoadServices() + { + if(_storageService == null) + { + _storageService = ServiceContainer.Resolve("storageService"); + } + } + + private async Task LoadSettingsAsync() + { + var now = DateTime.UtcNow; + if(_lastSettingsReload == null || (now - _lastSettingsReload.Value) > _settingsReloadSpan) + { + _lastSettingsReload = now; + _settingAutofillPasswordField = await _storageService.GetAsync( + Constants.AccessibilityAutofillPasswordFieldKey); + _settingAutofillPersistNotification = await _storageService.GetAsync( + Constants.AccessibilityAutofillPersistNotificationKey); + } + } + } +} diff --git a/src/Android/Accessibility/Browser.cs b/src/Android/Accessibility/Browser.cs new file mode 100644 index 000000000..f6bfd076b --- /dev/null +++ b/src/Android/Accessibility/Browser.cs @@ -0,0 +1,23 @@ +using System; + +namespace Bit.Droid.Accessibility +{ + public class Browser + { + public Browser(string packageName, string uriViewId) + { + PackageName = packageName; + UriViewId = uriViewId; + } + + public Browser(string packageName, string uriViewId, Func getUriFunction) + : this(packageName, uriViewId) + { + GetUriFunction = getUriFunction; + } + + public string PackageName { get; set; } + public string UriViewId { get; set; } + public Func GetUriFunction { get; set; } = (s) => s; + } +} diff --git a/src/Android/AutofillCredentials.cs b/src/Android/Accessibility/Credentials.cs similarity index 75% rename from src/Android/AutofillCredentials.cs rename to src/Android/Accessibility/Credentials.cs index 8ee4647ac..d265f07e8 100644 --- a/src/Android/AutofillCredentials.cs +++ b/src/Android/Accessibility/Credentials.cs @@ -1,6 +1,6 @@ -namespace Bit.Android +namespace Bit.Droid.Accessibility { - public class AutofillCredentials + public class Credentials { public string Username { get; set; } public string Password { get; set; } diff --git a/src/Android/Accessibility/NodeList.cs b/src/Android/Accessibility/NodeList.cs new file mode 100644 index 000000000..9843e912f --- /dev/null +++ b/src/Android/Accessibility/NodeList.cs @@ -0,0 +1,17 @@ +using Android.Views.Accessibility; +using System; +using System.Collections.Generic; + +namespace Bit.Droid.Accessibility +{ + public class NodeList : List, IDisposable + { + public void Dispose() + { + foreach(var item in this) + { + item.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index c1246e57a..72d6f8793 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -1,938 +1,174 @@  - + Debug AnyCPU - {04B18ED2-B76D-4947-8474-191F8FD2B5E0} + {304400AF-F0ED-40FA-B102-EA3C3EC43E4F} {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {c9e5eea5-ca05-42a1-839b-61506e0a37df} Library - Properties - Bit.Android + Bit.Droid BitwardenAndroid - 512 - true - Resources\Resource.Designer.cs - Off + True + Resources\Resource.designer.cs + Resource Properties\AndroidManifest.xml + Resources + Assets false v9.0 - armeabi,armeabi-v7a,x86 Xamarin.Android.Net.AndroidClientHandler - - - - - True - full + true + portable false - bin\Debug\ - DEBUG;TRACE + bin\Debug + DEBUG; prompt 4 - True None - PushNotification.Plugin;PushNotification.Plugin.Abstractions;Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;SQLite-net;Xamarin.Android.Net - True - False - False - Xamarin - False - 1G - Xamarin.Android.Net.AndroidClientHandler - False - False - False - btls + true + true pdbonly true - bin\Release\ - TRACE + bin\Release prompt 4 - False - Full - True - False - False - Xamarin - False - False - False - False - False - 1G - PushNotification.Plugin;PushNotification.Plugin.Abstractions;Xamarin.GooglePlayServices.Base;Xamarin.GooglePlayServices.Basement;Xamarin.GooglePlayServices.Measurement;Xamarin.GooglePlayServices.Gcm;BitwardenAndroid;BitwardenApp;SQLite-net;Xamarin.Android.Net - Xamarin.Android.Net.AndroidClientHandler - armeabi;armeabi-v7a;x86;x86_64;arm64-v8a - btls - SHA1withRSA - - - bin\FDroid\ - TRACE;FDROID - true - pdbonly - AnyCPU - Off - prompt - MinimumRecommendedRules.ruleset + true false - true - Full - BitwardenAndroid;BitwardenApp;SQLite-net;Xamarin.Android.Net - btls - armeabi;armeabi-v7a;x86;x86_64;arm64-v8a - 1G - SHA1withRSA - - False - .\Naxam.Ittianyu.BottomNavExtension.dll - - - + + - - - - - - + + 2.1.0.4 + + + 1.8.5 + + + 1.1.0 + + + 60.1142.1 + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - Designer - - - AndroidResource - - - AndroidResource - - - AndroidResource - - - + + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + {9F1742A7-7D03-4BB3-8FCD-41BC3002B00A} + App + + + {4b8a8c41-9820-4341-974c-41e65b7f4366} + Core + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {8a279ee4-4537-4656-9c93-44945e594556} - App - - - - - 60.1142.1 - - - 4.4.0 - - - 27.0.2.1 - - - 27.0.2.1 - - - 27.0.2.1 - - - 27.0.2.1 - - - 27.0.2.1 - - - 60.1142.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -977,16 +213,226 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1004,22 +450,49 @@ - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Assets/AboutAssets.txt b/src/Android/Assets/AboutAssets.txt deleted file mode 100644 index 5ddf08729..000000000 --- a/src/Android/Assets/AboutAssets.txt +++ /dev/null @@ -1,19 +0,0 @@ -Any raw assets you want to be deployed with your application can be placed in -this directory (and child directories) and given a Build Action of "AndroidAsset". - -These files will be deployed with you package and will be accessible using Android's -AssetManager, like this: - -public class ReadAsset : Activity -{ - protected override void OnCreate (Bundle bundle) - { - base.OnCreate (bundle); - - InputStream input = Assets.Open ("my_asset.txt"); - } -} - -Additionally, some Android functions will automatically load asset files: - -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/src/Android/Assets/FontAwesome.ttf b/src/Android/Assets/FontAwesome.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/src/Android/Assets/FontAwesome.ttf differ diff --git a/src/Android/Assets/MaterialIcons_Regular.ttf b/src/Android/Assets/MaterialIcons_Regular.ttf new file mode 100644 index 000000000..7015564ad Binary files /dev/null and b/src/Android/Assets/MaterialIcons_Regular.ttf differ diff --git a/src/Android/Assets/RobotoMono_Regular.ttf b/src/Android/Assets/RobotoMono_Regular.ttf new file mode 100644 index 000000000..f7b4a9b39 Binary files /dev/null and b/src/Android/Assets/RobotoMono_Regular.ttf differ diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index f441f8a3b..dd32301df 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Android.Content; using Android.Service.Autofill; using Android.Widget; using System.Linq; using Android.App; -using Bit.App.Abstractions; using System.Threading.Tasks; using Bit.App.Resources; -using Bit.App.Enums; +using Bit.Core.Enums; using Android.Views.Autofill; +using Bit.Core.Abstractions; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public static class AutofillHelpers { @@ -20,74 +19,91 @@ namespace Bit.Android.Autofill // These browser work natively with the autofill framework public static HashSet TrustedBrowsers = new HashSet { - "org.mozilla.focus","org.mozilla.klar","com.duckduckgo.mobile.android" + "org.mozilla.focus", + "org.mozilla.klar", + "com.duckduckgo.mobile.android", }; // These browsers work using the compatibility shim for the autofill framework public static HashSet CompatBrowsers = new HashSet { - "org.mozilla.firefox","org.mozilla.firefox_beta","com.microsoft.emmx","com.android.chrome", - "com.chrome.beta","com.android.browser","com.brave.browser","com.opera.browser", - "com.opera.browser.beta","com.opera.mini.native","com.chrome.dev","com.chrome.canary", - "com.google.android.apps.chrome","com.google.android.apps.chrome_dev","com.yandex.browser", - "com.sec.android.app.sbrowser","com.sec.android.app.sbrowser.beta","org.codeaurora.swe.browser", - "com.amazon.cloud9","mark.via.gp","org.bromite.bromite","org.chromium.chrome","com.kiwibrowser.browser", - "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.reference.browser", - "org.mozilla.rocket" + "org.mozilla.firefox", + "org.mozilla.firefox_beta", + "com.microsoft.emmx", + "com.android.chrome", + "com.chrome.beta", + "com.android.browser", + "com.brave.browser", + "com.opera.browser", + "com.opera.browser.beta", + "com.opera.mini.native", + "com.chrome.dev", + "com.chrome.canary", + "com.google.android.apps.chrome", + "com.google.android.apps.chrome_dev", + "com.yandex.browser", + "com.sec.android.app.sbrowser", + "com.sec.android.app.sbrowser.beta", + "org.codeaurora.swe.browser", + "com.amazon.cloud9", + "mark.via.gp", + "org.bromite.bromite", + "org.chromium.chrome", + "com.kiwibrowser.browser", + "com.ecosia.android", + "com.opera.mini.native.beta", + "org.mozilla.fennec_aurora", + "com.qwant.liberty", + "com.opera.touch", + "org.mozilla.fenix", + "org.mozilla.reference.browser", + "org.mozilla.rocket", }; // The URLs are blacklisted from autofilling public static HashSet BlacklistedUris = new HashSet { - "androidapp://android", "androidapp://com.x8bit.bitwarden", "androidapp://com.oneplus.applocker" + "androidapp://android", + "androidapp://com.x8bit.bitwarden", + "androidapp://com.oneplus.applocker", }; - public static async Task> GetFillItemsAsync(Parser parser, ICipherService service) + public static async Task> GetFillItemsAsync(Parser parser, ICipherService cipherService) { - var items = new List(); - if(parser.FieldCollection.FillableForLogin) { - var ciphers = await service.GetAllAsync(parser.Uri); + var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri); if(ciphers.Item1.Any() || ciphers.Item2.Any()) { var allCiphers = ciphers.Item1.ToList(); allCiphers.AddRange(ciphers.Item2.ToList()); - foreach(var cipher in allCiphers) - { - items.Add(new FilledItem(cipher)); - } + return allCiphers.Select(c => new FilledItem(c)).ToList(); } } else if(parser.FieldCollection.FillableForCard) { - var ciphers = await service.GetAllAsync(); - foreach(var cipher in ciphers.Where(c => c.Type == CipherType.Card)) - { - items.Add(new FilledItem(cipher)); - } + var ciphers = await cipherService.GetAllDecryptedAsync(); + return ciphers.Where(c => c.Type == CipherType.Card).Select(c => new FilledItem(c)).ToList(); } - - return items; + return new List(); } - public static FillResponse BuildFillResponse(Context context, Parser parser, List items, bool locked) + public static FillResponse BuildFillResponse(Parser parser, List items, bool locked) { var responseBuilder = new FillResponse.Builder(); if(items != null && items.Count > 0) { foreach(var item in items) { - var dataset = BuildDataset(context, parser.FieldCollection, item); + var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, item); if(dataset != null) { responseBuilder.AddDataset(dataset); } } } - - responseBuilder.AddDataset(BuildVaultDataset(context, parser.FieldCollection, parser.Uri, locked)); + responseBuilder.AddDataset(BuildVaultDataset(parser.ApplicationContext, parser.FieldCollection, + parser.Uri, locked)); AddSaveInfo(parser, responseBuilder, parser.FieldCollection); responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray()); return responseBuilder.Build(); @@ -96,7 +112,7 @@ namespace Bit.Android.Autofill public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem) { var datasetBuilder = new Dataset.Builder( - BuildListView(context.PackageName, filledItem.Name, filledItem.Subtitle, filledItem.Icon)); + BuildListView(filledItem.Name, filledItem.Subtitle, filledItem.Icon, context)); if(filledItem.ApplyToFields(fields, datasetBuilder)) { return datasetBuilder.Build(); @@ -128,8 +144,11 @@ namespace Bit.Android.Autofill var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, PendingIntentFlags.CancelCurrent); - var view = BuildListView(context.PackageName, AppResources.AutofillWithBitwarden, - locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault, Resource.Drawable.icon); + var view = BuildListView( + AppResources.AutofillWithBitwarden, + locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault, + Resource.Drawable.icon, + context); var datasetBuilder = new Dataset.Builder(view); datasetBuilder.SetAuthentication(pendingIntent.IntentSender); @@ -139,14 +158,14 @@ namespace Bit.Android.Autofill { datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER")); } - return datasetBuilder.Build(); } - public static RemoteViews BuildListView(string packageName, string text, string subtext, int iconId) + public static RemoteViews BuildListView(string text, string subtext, int iconId, Context context) { + var packageName = context.PackageName; var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem); - view.SetTextViewText(Resource.Id.text, text); + view.SetTextViewText(Resource.Id.text1, text); view.SetTextViewText(Resource.Id.text2, subtext); view.SetImageViewResource(Resource.Id.icon, iconId); return view; diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index cd0b05430..90e8009b6 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -5,20 +5,20 @@ using Android.OS; using Android.Runtime; using Android.Service.Autofill; using Android.Widget; -using Bit.App; -using Bit.App.Abstractions; -using Bit.App.Enums; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; using System.Collections.Generic; using System.Linq; -using XLabs.Ioc; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { [Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")] [IntentFilter(new string[] { "android.service.autofill.AutofillService" })] [MetaData("android.autofill", Resource = "@xml/autofillservice")] [Register("com.x8bit.bitwarden.Autofill.AutofillService")] - public class AutofillService : global::Android.Service.Autofill.AutofillService + public class AutofillService : Android.Service.Autofill.AutofillService { private ICipherService _cipherService; private ILockService _lockService; @@ -31,33 +31,32 @@ namespace Bit.Android.Autofill return; } - var parser = new Parser(structure); + var parser = new Parser(structure, ApplicationContext); parser.Parse(); if(!parser.ShouldAutofill) { return; } - + if(_lockService == null) { - _lockService = Resolver.Resolve(); + _lockService = ServiceContainer.Resolve("lockService"); } List items = null; - var locked = (await _lockService.GetLockTypeAsync(false)) != LockType.None; + var locked = await _lockService.IsLockedAsync(); if(!locked) { if(_cipherService == null) { - _cipherService = Resolver.Resolve(); + _cipherService = ServiceContainer.Resolve("cipherService"); } - items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService); } // build response - var response = AutofillHelpers.BuildFillResponse(this, parser, items, locked); + var response = AutofillHelpers.BuildFillResponse(parser, items, locked); callback.OnSuccess(response); } @@ -69,7 +68,7 @@ namespace Bit.Android.Autofill return; } - var parser = new Parser(structure); + var parser = new Parser(structure, ApplicationContext); parser.Parse(); var savedItem = parser.FieldCollection.GetSavedItem(); diff --git a/src/Android/Autofill/Field.cs b/src/Android/Autofill/Field.cs index 36d18de20..34a6d664c 100644 --- a/src/Android/Autofill/Field.cs +++ b/src/Android/Autofill/Field.cs @@ -7,7 +7,7 @@ using static Android.App.Assist.AssistStructure; using Android.Text; using static Android.Views.ViewStructure; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public class Field { @@ -86,6 +86,69 @@ namespace Bit.Android.Autofill public HtmlInfo HtmlInfo { get; private set; } public ViewNode Node { get; private set; } + public bool ValueIsNull() + { + return TextValue == null && DateValue == null && ToggleValue == null; + } + + public override bool Equals(object obj) + { + if(this == obj) + { + return true; + } + if(obj == null || GetType() != obj.GetType()) + { + return false; + } + var field = obj as Field; + if(TextValue != null ? !TextValue.Equals(field.TextValue) : field.TextValue != null) + { + return false; + } + if(DateValue != null ? !DateValue.Equals(field.DateValue) : field.DateValue != null) + { + return false; + } + return ToggleValue != null ? ToggleValue.Equals(field.ToggleValue) : field.ToggleValue == null; + } + + public override int GetHashCode() + { + var result = TextValue != null ? TextValue.GetHashCode() : 0; + result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0); + result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); + return result; + } + + private static List FilterForSupportedHints(string[] hints) + { + return hints?.Where(h => IsValidHint(h)).ToList() ?? new List(); + } + + private static bool IsValidHint(string hint) + { + switch(hint) + { + case View.AutofillHintCreditCardExpirationDate: + case View.AutofillHintCreditCardExpirationDay: + case View.AutofillHintCreditCardExpirationMonth: + case View.AutofillHintCreditCardExpirationYear: + case View.AutofillHintCreditCardNumber: + case View.AutofillHintCreditCardSecurityCode: + case View.AutofillHintEmailAddress: + case View.AutofillHintPhone: + case View.AutofillHintName: + case View.AutofillHintPassword: + case View.AutofillHintPostalAddress: + case View.AutofillHintPostalCode: + case View.AutofillHintUsername: + return true; + default: + return false; + } + } + private void UpdateSaveTypeFromHints() { SaveType = SaveDataType.Generic; @@ -128,72 +191,5 @@ namespace Bit.Android.Autofill } } } - - public bool ValueIsNull() - { - return TextValue == null && DateValue == null && ToggleValue == null; - } - - public override bool Equals(object obj) - { - if(this == obj) - { - return true; - } - - if(obj == null || GetType() != obj.GetType()) - { - return false; - } - - var field = obj as Field; - if(TextValue != null ? !TextValue.Equals(field.TextValue) : field.TextValue != null) - { - return false; - } - - if(DateValue != null ? !DateValue.Equals(field.DateValue) : field.DateValue != null) - { - return false; - } - - return ToggleValue != null ? ToggleValue.Equals(field.ToggleValue) : field.ToggleValue == null; - } - - public override int GetHashCode() - { - var result = TextValue != null ? TextValue.GetHashCode() : 0; - result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0); - result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); - return result; - } - - private static List FilterForSupportedHints(string[] hints) - { - return hints?.Where(h => IsValidHint(h)).ToList() ?? new List(); - } - - private static bool IsValidHint(string hint) - { - switch(hint) - { - case View.AutofillHintCreditCardExpirationDate: - case View.AutofillHintCreditCardExpirationDay: - case View.AutofillHintCreditCardExpirationMonth: - case View.AutofillHintCreditCardExpirationYear: - case View.AutofillHintCreditCardNumber: - case View.AutofillHintCreditCardSecurityCode: - case View.AutofillHintEmailAddress: - case View.AutofillHintPhone: - case View.AutofillHintName: - case View.AutofillHintPassword: - case View.AutofillHintPostalAddress: - case View.AutofillHintPostalCode: - case View.AutofillHintUsername: - return true; - default: - return false; - } - } } } diff --git a/src/Android/Autofill/FieldCollection.cs b/src/Android/Autofill/FieldCollection.cs index 05210073a..84e6f48db 100644 --- a/src/Android/Autofill/FieldCollection.cs +++ b/src/Android/Autofill/FieldCollection.cs @@ -5,7 +5,7 @@ using System.Linq; using Android.Text; using Android.Views; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public class FieldCollection { @@ -47,7 +47,6 @@ namespace Bit.Android.Autofill { return _passwordFields; } - if(Hints.Any()) { _passwordFields = new List(); @@ -64,7 +63,6 @@ namespace Bit.Android.Autofill _passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList(); } } - return _passwordFields; } } @@ -77,7 +75,6 @@ namespace Bit.Android.Autofill { return _usernameFields; } - _usernameFields = new List(); if(Hints.Any()) { @@ -102,20 +99,29 @@ namespace Bit.Android.Autofill } } } - return _usernameFields; } } - public bool FillableForLogin => FocusedHintsContain( - new string[] { View.AutofillHintUsername, View.AutofillHintEmailAddress, View.AutofillHintPassword }) || - UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused); - public bool FillableForCard => FocusedHintsContain( - new string[] { View.AutofillHintCreditCardNumber, View.AutofillHintCreditCardExpirationMonth, - View.AutofillHintCreditCardExpirationYear, View.AutofillHintCreditCardSecurityCode}); - public bool FillableForIdentity => FocusedHintsContain( - new string[] { View.AutofillHintName, View.AutofillHintPhone, View.AutofillHintPostalAddress, - View.AutofillHintPostalCode }); + public bool FillableForLogin => FocusedHintsContain(new string[] { + View.AutofillHintUsername, + View.AutofillHintEmailAddress, + View.AutofillHintPassword + }) || UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused); + + public bool FillableForCard => FocusedHintsContain(new string[] { + View.AutofillHintCreditCardNumber, + View.AutofillHintCreditCardExpirationMonth, + View.AutofillHintCreditCardExpirationYear, + View.AutofillHintCreditCardSecurityCode + }); + + public bool FillableForIdentity => FocusedHintsContain(new string[] { + View.AutofillHintName, + View.AutofillHintPhone, + View.AutofillHintPostalAddress, + View.AutofillHintPostalCode + }); public bool Fillable => FillableForLogin || FillableForCard || FillableForIdentity; @@ -127,7 +133,6 @@ namespace Bit.Android.Autofill } _passwordFields = _usernameFields = null; - FieldTrackingIds.Add(field.TrackingId); Fields.Add(field); AutofillIds.Add(field.AutofillId); @@ -141,12 +146,10 @@ namespace Bit.Android.Autofill { FocusedHints.Add(hint); } - if(!HintToFieldsMap.ContainsKey(hint)) { HintToFieldsMap.Add(hint, new List()); } - HintToFieldsMap[hint].Add(field); } } @@ -164,7 +167,7 @@ namespace Bit.Android.Autofill var savedItem = new SavedItem { - Type = App.Enums.CipherType.Login, + Type = Core.Enums.CipherType.Login, Login = new SavedItem.LoginItem { Password = GetFieldValue(passwordField) @@ -173,14 +176,13 @@ namespace Bit.Android.Autofill var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId).LastOrDefault(); savedItem.Login.Username = GetFieldValue(usernameField); - return savedItem; } else if(SaveType == SaveDataType.CreditCard) { var savedItem = new SavedItem { - Type = App.Enums.CipherType.Card, + Type = Core.Enums.CipherType.Card, Card = new SavedItem.CardItem { Number = GetFieldValue(View.AutofillHintCreditCardNumber), @@ -190,10 +192,8 @@ namespace Bit.Android.Autofill Code = GetFieldValue(View.AutofillHintCreditCardSecurityCode) } }; - return savedItem; } - return null; } @@ -224,7 +224,6 @@ namespace Bit.Android.Autofill } return fieldList.Select(f => f.AutofillId).ToArray(); } - return new AutofillId[0]; } @@ -238,7 +237,6 @@ namespace Bit.Android.Autofill { return HintToFieldsMap[View.AutofillHintCreditCardNumber].Select(f => f.AutofillId).ToArray(); } - return new AutofillId[0]; } @@ -260,7 +258,6 @@ namespace Bit.Android.Autofill } } } - return null; } @@ -270,7 +267,6 @@ namespace Bit.Android.Autofill { return null; } - if(!string.IsNullOrWhiteSpace(field.TextValue)) { if(field.AutofillType == AutofillType.List && field.ListValue.HasValue && monthValue) @@ -294,7 +290,6 @@ namespace Bit.Android.Autofill { return field.ToggleValue.Value.ToString(); } - return null; } @@ -340,7 +335,6 @@ namespace Bit.Android.Autofill { return false; } - var lowerValue = value.ToLowerInvariant(); return terms.Any(t => lowerValue.Contains(t)); } diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs index 854b4531d..ce2aa2671 100644 --- a/src/Android/Autofill/FilledItem.cs +++ b/src/Android/Autofill/FilledItem.cs @@ -1,107 +1,56 @@ -using System; -using Android.Service.Autofill; +using Android.Service.Autofill; using Android.Views.Autofill; using System.Linq; -using Bit.App.Models; -using Bit.App.Enums; +using Bit.Core.Enums; using Android.Views; +using Bit.Core.Models.View; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public class FilledItem { - private Lazy _password; - private Lazy _cardName; + private string _password; + private string _cardName; private string _cardNumber; - private Lazy _cardExpMonth; - private Lazy _cardExpYear; - private Lazy _cardCode; - private Lazy _idPhone; - private Lazy _idEmail; - private Lazy _idUsername; - private Lazy _idAddress; - private Lazy _idPostalCode; + private string _cardExpMonth; + private string _cardExpYear; + private string _cardCode; + private string _idPhone; + private string _idEmail; + private string _idUsername; + private string _idAddress; + private string _idPostalCode; - public FilledItem(Cipher cipher) + public FilledItem(CipherView cipher) { - Name = cipher.Name?.Decrypt(cipher.OrganizationId) ?? "--"; + Name = cipher.Name; Type = cipher.Type; + Subtitle = cipher.SubTitle; switch(Type) { case CipherType.Login: - Subtitle = cipher.Login.Username?.Decrypt(cipher.OrganizationId) ?? string.Empty; Icon = Resource.Drawable.login; - _password = new Lazy(() => cipher.Login.Password?.Decrypt(cipher.OrganizationId)); + _password = cipher.Login.Password; break; case CipherType.Card: - Subtitle = cipher.Card.Brand?.Decrypt(cipher.OrganizationId); - _cardNumber = cipher.Card.Number?.Decrypt(cipher.OrganizationId); - if(!string.IsNullOrWhiteSpace(_cardNumber) && _cardNumber.Length >= 4) - { - if(!string.IsNullOrWhiteSpace(_cardNumber)) - { - Subtitle += ", "; - } - Subtitle += ("*" + _cardNumber.Substring(_cardNumber.Length - 4)); - } + _cardNumber = cipher.Card.Number; Icon = Resource.Drawable.card; - _cardName = new Lazy(() => cipher.Card.CardholderName?.Decrypt(cipher.OrganizationId)); - _cardCode = new Lazy(() => cipher.Card.Code?.Decrypt(cipher.OrganizationId)); - _cardExpMonth = new Lazy(() => cipher.Card.ExpMonth?.Decrypt(cipher.OrganizationId)); - _cardExpYear = new Lazy(() => cipher.Card.ExpYear?.Decrypt(cipher.OrganizationId)); + _cardName = cipher.Card.CardholderName; + _cardCode = cipher.Card.Code; + _cardExpMonth = cipher.Card.ExpMonth; + _cardExpYear = cipher.Card.ExpYear; break; case CipherType.Identity: - var firstName = cipher.Identity?.FirstName?.Decrypt(cipher.OrganizationId) ?? " "; - var lastName = cipher.Identity?.LastName?.Decrypt(cipher.OrganizationId) ?? " "; - Subtitle = " "; - if(!string.IsNullOrWhiteSpace(firstName)) - { - Subtitle = firstName; - } - if(!string.IsNullOrWhiteSpace(lastName)) - { - if(!string.IsNullOrWhiteSpace(Subtitle)) - { - Subtitle += " "; - } - Subtitle += lastName; - } Icon = Resource.Drawable.id; - _idPhone = new Lazy(() => cipher.Identity.Phone?.Decrypt(cipher.OrganizationId)); - _idEmail = new Lazy(() => cipher.Identity.Email?.Decrypt(cipher.OrganizationId)); - _idUsername = new Lazy(() => cipher.Identity.Username?.Decrypt(cipher.OrganizationId)); - _idAddress = new Lazy(() => - { - var address = cipher.Identity.Address1?.Decrypt(cipher.OrganizationId); - - var address2 = cipher.Identity.Address2?.Decrypt(cipher.OrganizationId); - if(!string.IsNullOrWhiteSpace(address2)) - { - if(!string.IsNullOrWhiteSpace(address)) - { - address += ", "; - } - - address += address2; - } - - var address3 = cipher.Identity.Address3?.Decrypt(cipher.OrganizationId); - if(!string.IsNullOrWhiteSpace(address3)) - { - if(!string.IsNullOrWhiteSpace(address)) - { - address += ", "; - } - - address += address3; - } - - return address; - }); - _idPostalCode = new Lazy(() => cipher.Identity.PostalCode?.Decrypt(cipher.OrganizationId)); + _idPhone = cipher.Identity.Phone; + _idEmail = cipher.Identity.Email; + _idUsername = cipher.Identity.Username; + _idAddress = cipher.Identity.FullAddress; + _idPostalCode = cipher.Identity.PostalCode; break; default: + Icon = Resource.Drawable.login; break; } } @@ -121,11 +70,11 @@ namespace Bit.Android.Autofill var setValues = false; if(Type == CipherType.Login) { - if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password.Value)) + if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password)) { foreach(var f in fieldCollection.PasswordFields) { - var val = ApplyValue(f, _password.Value); + var val = ApplyValue(f, _password); if(val != null) { setValues = true; @@ -133,7 +82,6 @@ namespace Bit.Android.Autofill } } } - if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle)) { foreach(var f in fieldCollection.UsernameFields) @@ -149,68 +97,73 @@ namespace Bit.Android.Autofill } else if(Type == CipherType.Card) { - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardNumber, - new Lazy(() => _cardNumber))) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardNumber, + _cardNumber)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardSecurityCode, _cardCode)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardSecurityCode, + _cardCode)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardExpirationMonth, _cardExpMonth, true)) + if(ApplyValue(datasetBuilder, fieldCollection, + Android.Views.View.AutofillHintCreditCardExpirationMonth, _cardExpMonth, true)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardExpirationYear, _cardExpYear)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardExpirationYear, + _cardExpYear)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintName, _cardName)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintName, _cardName)) { setValues = true; } } else if(Type == CipherType.Identity) { - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPhone, _idPhone)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPhone, _idPhone)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintEmailAddress, _idEmail)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintEmailAddress, _idEmail)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintUsername, _idUsername)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintUsername, + _idUsername)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPostalAddress, _idAddress)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPostalAddress, + _idAddress)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPostalCode, _idPostalCode)) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPostalCode, + _idPostalCode)) { setValues = true; } - if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintName, new Lazy(() => Subtitle))) + if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintName, Subtitle)) { setValues = true; } } - return setValues; } private static bool ApplyValue(Dataset.Builder builder, FieldCollection fieldCollection, - string hint, Lazy value, bool monthValue = false) + string hint, string value, bool monthValue = false) { bool setValues = false; - if(fieldCollection.HintToFieldsMap.ContainsKey(hint) && !string.IsNullOrWhiteSpace(value.Value)) + if(fieldCollection.HintToFieldsMap.ContainsKey(hint) && !string.IsNullOrWhiteSpace(value)) { foreach(var f in fieldCollection.HintToFieldsMap[hint]) { - var val = ApplyValue(f, value.Value, monthValue); + var val = ApplyValue(f, value, monthValue); if(val != null) { setValues = true; @@ -245,7 +198,6 @@ namespace Bit.Android.Autofill return AutofillValue.ForList(monthIndex - 1); } } - for(var i = 0; i < field.AutofillOptions.Count; i++) { if(field.AutofillOptions[i].Equals(value)) @@ -266,7 +218,6 @@ namespace Bit.Android.Autofill default: break; } - return null; } } diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs index 92d80c9b7..228bb7817 100644 --- a/src/Android/Autofill/Parser.cs +++ b/src/Android/Autofill/Parser.cs @@ -1,29 +1,31 @@ using static Android.App.Assist.AssistStructure; using Android.App.Assist; -using Bit.App; using System.Collections.Generic; +using Bit.Core; +using Android.Content; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public class Parser { - - public static HashSet ExcludedPackageIds = new HashSet + public static HashSet _excludedPackageIds = new HashSet { "android" }; - private readonly AssistStructure _structure; private string _uri; private string _packageName; private string _webDomain; - public Parser(AssistStructure structure) + public Parser(AssistStructure structure, Context applicationContext) { _structure = structure; + ApplicationContext = applicationContext; } + public Context ApplicationContext { get; set; } public FieldCollection FieldCollection { get; private set; } = new FieldCollection(); + public string Uri { get @@ -32,12 +34,12 @@ namespace Bit.Android.Autofill { return _uri; } - - if(string.IsNullOrWhiteSpace(WebDomain) && string.IsNullOrWhiteSpace(PackageName)) + var webDomainNull = string.IsNullOrWhiteSpace(WebDomain); + if(webDomainNull && string.IsNullOrWhiteSpace(PackageName)) { _uri = null; } - else if(!string.IsNullOrWhiteSpace(WebDomain)) + else if(!webDomainNull) { _uri = string.Concat("http://", WebDomain); } @@ -45,10 +47,10 @@ namespace Bit.Android.Autofill { _uri = string.Concat(Constants.AndroidAppProtocol, PackageName); } - return _uri; } } + public string PackageName { get => _packageName; @@ -58,10 +60,10 @@ namespace Bit.Android.Autofill { _packageName = _uri = null; } - _packageName = value; } } + public string WebDomain { get => _webDomain; @@ -71,19 +73,12 @@ namespace Bit.Android.Autofill { _webDomain = _uri = null; } - _webDomain = value; } } - public bool ShouldAutofill - { - get - { - return !string.IsNullOrWhiteSpace(Uri) && !AutofillHelpers.BlacklistedUris.Contains(Uri) && - FieldCollection != null && FieldCollection.Fillable; - } - } + public bool ShouldAutofill => !string.IsNullOrWhiteSpace(Uri) && + !AutofillHelpers.BlacklistedUris.Contains(Uri) && FieldCollection != null && FieldCollection.Fillable; public void Parse() { @@ -92,7 +87,6 @@ namespace Bit.Android.Autofill var node = _structure.GetWindowNodeAt(i); ParseNode(node.RootViewNode); } - if(!AutofillHelpers.TrustedBrowsers.Contains(PackageName) && !AutofillHelpers.CompatBrowsers.Contains(PackageName)) { @@ -123,7 +117,7 @@ namespace Bit.Android.Autofill private void SetPackageAndDomain(ViewNode node) { if(string.IsNullOrWhiteSpace(PackageName) && !string.IsNullOrWhiteSpace(node.IdPackage) && - !ExcludedPackageIds.Contains(node.IdPackage)) + !_excludedPackageIds.Contains(node.IdPackage)) { PackageName = node.IdPackage; } @@ -133,4 +127,4 @@ namespace Bit.Android.Autofill } } } -} \ No newline at end of file +} diff --git a/src/Android/Autofill/SavedItem.cs b/src/Android/Autofill/SavedItem.cs index 280335199..482bc3a5c 100644 --- a/src/Android/Autofill/SavedItem.cs +++ b/src/Android/Autofill/SavedItem.cs @@ -1,6 +1,6 @@ -using Bit.App.Enums; +using Bit.Core.Enums; -namespace Bit.Android.Autofill +namespace Bit.Droid.Autofill { public class SavedItem { diff --git a/src/Android/Controls/CustomButtonRenderer.cs b/src/Android/Controls/CustomButtonRenderer.cs deleted file mode 100644 index 18b681662..000000000 --- a/src/Android/Controls/CustomButtonRenderer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.ComponentModel; -using Android.Content; -using Bit.Android.Controls; -using Xamarin.Forms; -using Xamarin.Forms.Platform.Android; - -[assembly: ExportRenderer(typeof(Button), typeof(CustomButtonRenderer))] -namespace Bit.Android.Controls -{ - public class CustomButtonRenderer : ButtonRenderer - { - public CustomButtonRenderer(Context context) - : base(context) - { } - - protected override void OnElementChanged(ElementChangedEventArgs + + + + + + diff --git a/src/App/Pages/Accounts/HomePage.xaml.cs b/src/App/Pages/Accounts/HomePage.xaml.cs new file mode 100644 index 000000000..20bdfde65 --- /dev/null +++ b/src/App/Pages/Accounts/HomePage.xaml.cs @@ -0,0 +1,37 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class HomePage : BaseContentPage + { + public HomePage() + { + InitializeComponent(); + } + + private void LogIn_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + Navigation.PushModalAsync(new NavigationPage(new LoginPage())); + } + } + + private void Register_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + Navigation.PushModalAsync(new NavigationPage(new RegisterPage())); + } + } + + private void Settings_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + Navigation.PushModalAsync(new NavigationPage(new EnvironmentPage())); + } + } + } +} diff --git a/src/App/Pages/Accounts/HomePageViewModel.cs b/src/App/Pages/Accounts/HomePageViewModel.cs new file mode 100644 index 000000000..3dbd0c946 --- /dev/null +++ b/src/App/Pages/Accounts/HomePageViewModel.cs @@ -0,0 +1,12 @@ +using System; + +namespace Bit.App.Pages +{ + public class HomeViewModel : BaseViewModel + { + public HomeViewModel() + { + PageTitle = "Home Page"; + } + } +} diff --git a/src/App/Pages/Accounts/LockPage.xaml b/src/App/Pages/Accounts/LockPage.xaml new file mode 100644 index 000000000..b9ad4d43d --- /dev/null +++ b/src/App/Pages/Accounts/LockPage.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Accounts/LockPage.xaml.cs b/src/App/Pages/Accounts/LockPage.xaml.cs new file mode 100644 index 000000000..41714ec94 --- /dev/null +++ b/src/App/Pages/Accounts/LockPage.xaml.cs @@ -0,0 +1,91 @@ +using Bit.App.Models; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class LockPage : BaseContentPage + { + private readonly AppOptions _appOptions; + private readonly LockPageViewModel _vm; + + public LockPage(AppOptions appOptions = null) + { + _appOptions = appOptions; + 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(); + }; + MasterPasswordEntry = _masterPassword; + PinEntry = _pin; + } + + public Entry MasterPasswordEntry { get; set; } + public Entry PinEntry { get; set; } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await _vm.InitAsync(); + if(!_vm.FingerprintLock) + { + if(_vm.PinLock) + { + RequestFocus(PinEntry); + } + else + { + RequestFocus(MasterPasswordEntry); + } + } + } + + private void Unlock_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + var tasks = Task.Run(async () => + { + await Task.Delay(50); + Device.BeginInvokeOnMainThread(async () => + { + await _vm.SubmitAsync(); + }); + }); + } + } + + private async void LogOut_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.LogOutAsync(); + } + } + + private async void Fingerprint_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.PromptFingerprintAsync(); + } + } + } +} diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs new file mode 100644 index 000000000..2939ca900 --- /dev/null +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -0,0 +1,259 @@ +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class LockPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IDeviceActionService _deviceActionService; + private readonly ILockService _lockService; + private readonly ICryptoService _cryptoService; + private readonly IStorageService _storageService; + private readonly IUserService _userService; + private readonly IMessagingService _messagingService; + + private bool _hasKey; + private string _email; + private bool _showPassword; + private bool _pinLock; + private bool _fingerprintLock; + private string _fingerprintButtonText; + private string _loggedInAsText; + private string _lockedVerifyText; + private int _invalidPinAttempts = 0; + private Tuple _pinSet; + + public LockPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _lockService = ServiceContainer.Resolve("lockService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _storageService = ServiceContainer.Resolve("storageService"); + _userService = ServiceContainer.Resolve("userService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + + PageTitle = AppResources.VerifyMasterPassword; + TogglePasswordCommand = new Command(TogglePassword); + } + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + + public bool PinLock + { + get => _pinLock; + set => SetProperty(ref _pinLock, value); + } + + public bool FingerprintLock + { + get => _fingerprintLock; + set => SetProperty(ref _fingerprintLock, value); + } + + public string FingerprintButtonText + { + get => _fingerprintButtonText; + set => SetProperty(ref _fingerprintButtonText, value); + } + + public string LoggedInAsText + { + get => _loggedInAsText; + set => SetProperty(ref _loggedInAsText, value); + } + + public string LockedVerifyText + { + get => _lockedVerifyText; + set => SetProperty(ref _lockedVerifyText, value); + } + + public Command TogglePasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string MasterPassword { get; set; } + public string Pin { get; set; } + public Action UnlockedAction { get; set; } + + public async Task InitAsync() + { + _pinSet = await _lockService.IsPinLockSetAsync(); + _hasKey = await _cryptoService.HasKeyAsync(); + PinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2; + FingerprintLock = await _lockService.IsFingerprintLockSetAsync(); + _email = await _userService.GetEmailAsync(); + LoggedInAsText = string.Format(AppResources.LoggedInAs, _email); + if(PinLock) + { + PageTitle = AppResources.VerifyPIN; + LockedVerifyText = AppResources.VaultLockedPIN; + } + else + { + PageTitle = AppResources.VerifyMasterPassword; + LockedVerifyText = AppResources.VaultLockedMasterPassword; + } + + if(FingerprintLock) + { + FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock : + AppResources.UseFingerprintToUnlock; + var tasks = Task.Run(async () => + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(async () => await PromptFingerprintAsync()); + }); + } + } + + public async Task SubmitAsync() + { + if(PinLock && string.IsNullOrWhiteSpace(Pin)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), + AppResources.Ok); + return; + } + if(!PinLock && string.IsNullOrWhiteSpace(MasterPassword)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.Ok); + return; + } + + var kdf = await _userService.GetKdfAsync(); + var kdfIterations = await _userService.GetKdfIterationsAsync(); + + if(PinLock) + { + var failed = true; + try + { + if(_pinSet.Item1) + { + var protectedPin = await _storageService.GetAsync(Constants.ProtectedPin); + var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin)); + failed = decPin != Pin; + _lockService.PinLocked = failed; + if(!failed) + { + DoContinue(); + } + } + else + { + var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, + kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); + failed = false; + await SetKeyAndContinueAsync(key); + } + } + catch + { + failed = true; + } + if(failed) + { + _invalidPinAttempts++; + if(_invalidPinAttempts >= 5) + { + _messagingService.Send("logout"); + return; + } + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN, + AppResources.AnErrorHasOccurred); + } + } + else + { + var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations); + var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); + if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash) + { + await SetKeyAndContinueAsync(key); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword, + AppResources.AnErrorHasOccurred); + } + } + } + + public async Task LogOutAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, + AppResources.LogOut, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _messagingService.Send("logout"); + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + var page = (Page as LockPage); + var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry; + entry.Focus(); + } + + public async Task PromptFingerprintAsync() + { + var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, + PinLock ? AppResources.PIN : AppResources.MasterPassword, () => + { + var page = Page as LockPage; + if(PinLock) + { + page.PinEntry.Focus(); + } + else + { + page.MasterPasswordEntry.Focus(); + } + }); + _lockService.FingerprintLocked = !success; + if(success) + { + DoContinue(); + } + } + + private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key) + { + if(!_hasKey) + { + await _cryptoService.SetKeyAsync(key); + } + DoContinue(); + } + + private void DoContinue() + { + _messagingService.Send("unlocked"); + UnlockedAction?.Invoke(); + } + } +} diff --git a/src/App/Pages/Accounts/LoginPage.xaml b/src/App/Pages/Accounts/LoginPage.xaml new file mode 100644 index 000000000..9b8d058c9 --- /dev/null +++ b/src/App/Pages/Accounts/LoginPage.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Accounts/LoginPage.xaml.cs b/src/App/Pages/Accounts/LoginPage.xaml.cs new file mode 100644 index 000000000..bc1877279 --- /dev/null +++ b/src/App/Pages/Accounts/LoginPage.xaml.cs @@ -0,0 +1,50 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class LoginPage : BaseContentPage + { + private LoginPageViewModel _vm; + + public LoginPage() + { + InitializeComponent(); + _vm = BindingContext as LoginPageViewModel; + _vm.Page = this; + MasterPasswordEntry = _masterPassword; + } + + public Entry MasterPasswordEntry { get; set; } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await _vm.InitAsync(); + if(string.IsNullOrWhiteSpace(_vm.Email)) + { + RequestFocus(_email); + } + else + { + RequestFocus(_masterPassword); + } + } + + private async void LogIn_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.LogInAsync(); + } + } + + private void Hint_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + Navigation.PushModalAsync(new NavigationPage(new HintPage())); + } + } + } +} diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs new file mode 100644 index 000000000..713599a9a --- /dev/null +++ b/src/App/Pages/Accounts/LoginPageViewModel.cs @@ -0,0 +1,132 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class LoginPageViewModel : BaseViewModel + { + private const string Keys_RememberedEmail = "rememberedEmail"; + private const string Keys_RememberEmail = "rememberEmail"; + + private readonly IDeviceActionService _deviceActionService; + private readonly IAuthService _authService; + private readonly ISyncService _syncService; + private readonly IStorageService _storageService; + + private bool _showPassword; + private string _email; + private string _masterPassword; + + public LoginPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _authService = ServiceContainer.Resolve("authService"); + _syncService = ServiceContainer.Resolve("syncService"); + _storageService = ServiceContainer.Resolve("storageService"); + + PageTitle = AppResources.Bitwarden; + TogglePasswordCommand = new Command(TogglePassword); + } + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + + public string Email + { + get => _email; + set => SetProperty(ref _email, value); + } + + public string MasterPassword + { + get => _masterPassword; + set => SetProperty(ref _masterPassword, value); + } + + public Command TogglePasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public bool RememberEmail { get; set; } + + public async Task InitAsync() + { + if(string.IsNullOrWhiteSpace(Email)) + { + Email = await _storageService.GetAsync(Keys_RememberedEmail); + } + var rememberEmail = await _storageService.GetAsync(Keys_RememberEmail); + RememberEmail = rememberEmail.GetValueOrDefault(true); + } + + public async Task LogInAsync() + { + if(string.IsNullOrWhiteSpace(Email)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress), + AppResources.Ok); + return; + } + if(!Email.Contains("@")) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok); + return; + } + if(string.IsNullOrWhiteSpace(MasterPassword)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.Ok); + return; + } + + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + var response = await _authService.LogInAsync(Email, MasterPassword); + MasterPassword = string.Empty; + if(RememberEmail) + { + await _storageService.SaveAsync(Keys_RememberedEmail, Email); + } + else + { + await _storageService.RemoveAsync(Keys_RememberedEmail); + } + await _deviceActionService.HideLoadingAsync(); + if(response.TwoFactor) + { + var page = new TwoFactorPage(); + await Page.Navigation.PushModalAsync(new NavigationPage(page)); + } + else + { + var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); + Application.Current.MainPage = new TabsPage(); + } + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + (Page as LoginPage).MasterPasswordEntry.Focus(); + } + } +} diff --git a/src/App/Pages/Accounts/RegisterPage.xaml b/src/App/Pages/Accounts/RegisterPage.xaml new file mode 100644 index 000000000..ebfa23f5a --- /dev/null +++ b/src/App/Pages/Accounts/RegisterPage.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Accounts/RegisterPage.xaml.cs b/src/App/Pages/Accounts/RegisterPage.xaml.cs new file mode 100644 index 000000000..e8ee996e3 --- /dev/null +++ b/src/App/Pages/Accounts/RegisterPage.xaml.cs @@ -0,0 +1,36 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class RegisterPage : BaseContentPage + { + private RegisterPageViewModel _vm; + + public RegisterPage() + { + InitializeComponent(); + _vm = BindingContext as RegisterPageViewModel; + _vm.Page = this; + MasterPasswordEntry = _masterPassword; + ConfirmMasterPasswordEntry = _confirmMasterPassword; + } + + public Entry MasterPasswordEntry { get; set; } + public Entry ConfirmMasterPasswordEntry { get; set; } + + protected override void OnAppearing() + { + base.OnAppearing(); + RequestFocus(_email); + } + + private async void Submit_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + } +} diff --git a/src/App/Pages/Accounts/RegisterPageViewModel.cs b/src/App/Pages/Accounts/RegisterPageViewModel.cs new file mode 100644 index 000000000..ced9122b0 --- /dev/null +++ b/src/App/Pages/Accounts/RegisterPageViewModel.cs @@ -0,0 +1,138 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Request; +using Bit.Core.Utilities; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class RegisterPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly IApiService _apiService; + private readonly ICryptoService _cryptoService; + + private bool _showPassword; + + public RegisterPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _apiService = ServiceContainer.Resolve("apiService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + + PageTitle = AppResources.Bitwarden; + TogglePasswordCommand = new Command(TogglePassword); + ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword); + } + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + + public Command TogglePasswordCommand { get; } + public Command ToggleConfirmPasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string Name { get; set; } + public string Email { get; set; } + public string MasterPassword { get; set; } + public string ConfirmMasterPassword { get; set; } + public string Hint { get; set; } + + public async Task SubmitAsync() + { + if(string.IsNullOrWhiteSpace(Email)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress), + AppResources.Ok); + return; + } + if(!Email.Contains("@")) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok); + return; + } + if(string.IsNullOrWhiteSpace(MasterPassword)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.Ok); + return; + } + if(MasterPassword.Length < 8) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + AppResources.MasterPasswordLengthValMessage, AppResources.Ok); + return; + } + if(MasterPassword != ConfirmMasterPassword) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok); + return; + } + + // TODO: Password strength check? + + Name = string.IsNullOrWhiteSpace(Name) ? null : Name; + Email = Email.Trim().ToLower(); + var kdf = KdfType.PBKDF2_SHA256; + var kdfIterations = 100_000; + var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdf, kdfIterations); + var encKey = await _cryptoService.MakeEncKeyAsync(key); + var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1); + var request = new RegisterRequest + { + Email = Email, + Name = Name, + MasterPasswordHash = hashedPassword, + MasterPasswordHint = Hint, + Key = encKey.Item2.EncryptedString, + Kdf = kdf, + KdfIterations = kdfIterations, + Keys = new KeysRequest + { + PublicKey = keys.Item1, + EncryptedPrivateKey = keys.Item2.EncryptedString + } + }; + // TODO: org invite? + + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + await _apiService.PostRegisterAsync(request); + await _deviceActionService.HideLoadingAsync(); + // TODO: dismiss this page from home page and pass email for login page + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + (Page as RegisterPage).MasterPasswordEntry.Focus(); + } + + public void ToggleConfirmPassword() + { + ShowPassword = !ShowPassword; + (Page as RegisterPage).ConfirmMasterPasswordEntry.Focus(); + } + } +} diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml b/src/App/Pages/Accounts/TwoFactorPage.xaml new file mode 100644 index 000000000..004eeecba --- /dev/null +++ b/src/App/Pages/Accounts/TwoFactorPage.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs new file mode 100644 index 000000000..23913b9eb --- /dev/null +++ b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs @@ -0,0 +1,127 @@ +using Bit.App.Controls; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class TwoFactorPage : BaseContentPage + { + private readonly IBroadcasterService _broadcasterService; + private readonly IMessagingService _messagingService; + + private TwoFactorPageViewModel _vm; + + public TwoFactorPage() + { + InitializeComponent(); + SetActivityIndicator(); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _vm = BindingContext as TwoFactorPageViewModel; + _vm.Page = this; + DuoWebView = _duoWebView; + } + + public HybridWebView DuoWebView { get; set; } + + public void AddContinueButton() + { + if(ToolbarItems.Count == 0) + { + ToolbarItems.Add(_continueItem); + } + } + + public void RemoveContinueButton() + { + if(ToolbarItems.Count > 0) + { + ToolbarItems.Remove(_continueItem); + } + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + _broadcasterService.Subscribe(nameof(TwoFactorPage), async (message) => + { + if(message.Command == "gotYubiKeyOTP") + { + if(_vm.YubikeyMethod) + { + _vm.Token = (string)message.Data; + await _vm.SubmitAsync(); + } + } + else if(message.Command == "resumeYubiKey") + { + if(_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", true); + } + } + }); + await LoadOnAppearedAsync(_scrollView, true, () => + { + _vm.Init(); + if(_vm.TotpMethod) + { + RequestFocus(_totpEntry); + } + return Task.FromResult(0); + }); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + if(!_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + } + } + + protected override bool OnBackButtonPressed() + { + // ref: https://github.com/bitwarden/mobile/issues/350 + if(_vm.YubikeyMethod) + { + if(Device.RuntimePlatform == Device.Android) + { + return true; + } + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + } + return base.OnBackButtonPressed(); + } + + private async void Continue_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void Methods_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.AnotherMethodAsync(); + } + } + + private async void ResendEmail_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SendEmailAsync(true, true); + } + } + } +} diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs new file mode 100644 index 000000000..d187e26a8 --- /dev/null +++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs @@ -0,0 +1,267 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Request; +using Bit.Core.Utilities; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class TwoFactorPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly IAuthService _authService; + private readonly ISyncService _syncService; + private readonly IStorageService _storageService; + private readonly IApiService _apiService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IEnvironmentService _environmentService; + private readonly IMessagingService _messagingService; + private readonly IBroadcasterService _broadcasterService; + + private bool _u2fSupported = false; + private TwoFactorProviderType? _selectedProviderType; + private string _totpInstruction; + private string _webVaultUrl = "https://vault.bitwarden.com"; + + public TwoFactorPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _authService = ServiceContainer.Resolve("authService"); + _syncService = ServiceContainer.Resolve("syncService"); + _storageService = ServiceContainer.Resolve("storageService"); + _apiService = ServiceContainer.Resolve("apiService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _environmentService = ServiceContainer.Resolve("environmentService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + + PageTitle = AppResources.TwoStepLogin; + } + + public string TotpInstruction + { + get => _totpInstruction; + set => SetProperty(ref _totpInstruction, value); + } + + public bool Remember { get; set; } + + public string Token { get; set; } + + public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo || + SelectedProviderType == TwoFactorProviderType.OrganizationDuo; + + public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey; + + public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator; + + public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email; + + public bool TotpMethod => AuthenticatorMethod || EmailMethod; + + public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : + AppResources.YubiKeyInstruction; + + public TwoFactorProviderType? SelectedProviderType + { + get => _selectedProviderType; + set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[] + { + nameof(EmailMethod), + nameof(DuoMethod), + nameof(YubikeyMethod), + nameof(AuthenticatorMethod), + nameof(TotpMethod), + }); + } + + public void Init() + { + if(string.IsNullOrWhiteSpace(_authService.Email) || + string.IsNullOrWhiteSpace(_authService.MasterPasswordHash) || + _authService.TwoFactorProvidersData == null) + { + // TODO: dismiss modal? + return; + } + + if(!string.IsNullOrWhiteSpace(_environmentService.BaseUrl)) + { + _webVaultUrl = _environmentService.BaseUrl; + } + else if(!string.IsNullOrWhiteSpace(_environmentService.WebVaultUrl)) + { + _webVaultUrl = _environmentService.WebVaultUrl; + } + + // TODO: init U2F + _u2fSupported = false; + + SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported); + Load(); + } + + public void Load() + { + if(SelectedProviderType == null) + { + PageTitle = AppResources.LoginUnavailable; + return; + } + var page = Page as TwoFactorPage; + PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name; + var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value]; + switch(SelectedProviderType.Value) + { + case TwoFactorProviderType.U2f: + // TODO + break; + case TwoFactorProviderType.YubiKey: + _messagingService.Send("listenYubiKeyOTP", true); + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + 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 => + { + Token = sig; + await SubmitAsync(); + }); + break; + case TwoFactorProviderType.Email: + TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail, + providerData["Email"] as string); + if(_authService.TwoFactorProvidersData.Count > 1) + { + var emailTask = Task.Run(() => SendEmailAsync(false, false)); + } + break; + case TwoFactorProviderType.Authenticator: + TotpInstruction = AppResources.EnterVerificationCodeApp; + break; + default: + break; + } + + if(!YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", false); + } + if(DuoMethod) + { + page.RemoveContinueButton(); + } + else + { + page.AddContinueButton(); + } + } + + public async Task SubmitAsync() + { + if(SelectedProviderType == null) + { + return; + } + if(string.IsNullOrWhiteSpace(Token)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode), + AppResources.AnErrorHasOccurred); + } + if(SelectedProviderType == TwoFactorProviderType.Email || + SelectedProviderType == TwoFactorProviderType.Authenticator) + { + Token = Token.Replace(" ", string.Empty).Trim(); + } + + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember); + await _deviceActionService.HideLoadingAsync(); + var task = Task.Run(() => _syncService.FullSyncAsync(true)); + _messagingService.Send("listenYubiKeyOTP", false); + _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); + Application.Current.MainPage = new TabsPage(); + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred); + } + } + + public async Task AnotherMethodAsync() + { + var supportedProviders = _authService.GetSupportedTwoFactorProviders(); + var options = supportedProviders.Select(p => p.Name).ToList(); + options.Add(AppResources.RecoveryCodeTitle); + var method = await Page.DisplayActionSheet(AppResources.TwoStepLoginOptions, AppResources.Cancel, + null, options.ToArray()); + if(method == AppResources.RecoveryCodeTitle) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/lost-two-step-device/"); + } + else if(method != AppResources.Cancel) + { + var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type; + if(selected == SelectedProviderType) + { + // Nothing changed + return; + } + SelectedProviderType = selected; + Load(); + } + } + + public async Task SendEmailAsync(bool showLoading, bool doToast) + { + if(!EmailMethod) + { + return false; + } + try + { + if(showLoading) + { + await _deviceActionService.ShowLoadingAsync(AppResources.Submitting); + } + var request = new TwoFactorEmailRequest + { + Email = _authService.Email, + MasterPasswordHash = _authService.MasterPasswordHash + }; + await _apiService.PostTwoFactorEmailAsync(request); + if(showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + if(doToast) + { + _platformUtilsService.ShowToast("success", null, AppResources.VerificationEmailSent); + } + return true; + } + catch(ApiException) + { + if(showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent); + return false; + } + } + } +} diff --git a/src/App/Pages/BaseContentPage.cs b/src/App/Pages/BaseContentPage.cs new file mode 100644 index 000000000..333712807 --- /dev/null +++ b/src/App/Pages/BaseContentPage.cs @@ -0,0 +1,119 @@ +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class BaseContentPage : ContentPage + { + private IStorageService _storageService; + + protected int AndroidShowModalAnimationDelay = 400; + protected int AndroidShowPageAnimationDelay = 100; + + public DateTime? LastPageAction { get; set; } + + protected override void OnAppearing() + { + base.OnAppearing(); + SaveActivity(); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + SaveActivity(); + } + + public bool DoOnce(Action action = null, int milliseconds = 1000) + { + if(LastPageAction.HasValue && (DateTime.UtcNow - LastPageAction.Value).TotalMilliseconds < milliseconds) + { + // Last action occurred recently. + return false; + } + LastPageAction = DateTime.UtcNow; + action?.Invoke(); + return true; + } + + protected void SetActivityIndicator(ContentView targetView = null) + { + var indicator = new ActivityIndicator + { + IsRunning = true, + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.Center + }; + if(targetView != null) + { + targetView.Content = indicator; + } + else + { + Content = indicator; + } + } + + protected async Task LoadOnAppearedAsync(View sourceView, bool fromModal, Func workFunction, + ContentView targetView = null) + { + async Task DoWorkAsync() + { + await workFunction.Invoke(); + if(sourceView != null) + { + if(targetView != null) + { + targetView.Content = sourceView; + } + else + { + Content = sourceView; + } + } + } + if(Device.RuntimePlatform == Device.iOS) + { + await DoWorkAsync(); + return; + } + await Task.Run(async () => + { + await Task.Delay(fromModal ? AndroidShowModalAnimationDelay : AndroidShowPageAnimationDelay); + 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); + Device.BeginInvokeOnMainThread(() => input.Focus()); + }); + } + + private void SetStorageService() + { + if(_storageService == null) + { + _storageService = ServiceContainer.Resolve("storageService"); + } + } + + private void SaveActivity() + { + SetStorageService(); + _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow); + } + } +} diff --git a/src/App/Pages/BaseViewModel.cs b/src/App/Pages/BaseViewModel.cs new file mode 100644 index 000000000..da6a1d453 --- /dev/null +++ b/src/App/Pages/BaseViewModel.cs @@ -0,0 +1,18 @@ +using Bit.Core.Utilities; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public abstract class BaseViewModel : ExtendedViewModel + { + private string _pageTitle = string.Empty; + + public string PageTitle + { + get => _pageTitle; + set => SetProperty(ref _pageTitle, value); + } + + public ContentPage Page { get; set; } + } +} diff --git a/src/App/Pages/CollectionViewModel.cs b/src/App/Pages/CollectionViewModel.cs new file mode 100644 index 000000000..5b072f0fc --- /dev/null +++ b/src/App/Pages/CollectionViewModel.cs @@ -0,0 +1,16 @@ +using Bit.Core.Utilities; + +namespace Bit.App.Pages +{ + public class CollectionViewModel : ExtendedViewModel + { + private bool _checked; + + public Core.Models.View.CollectionView Collection { get; set; } + public bool Checked + { + get => _checked; + set => SetProperty(ref _checked, value); + } + } +} diff --git a/src/App/Pages/EnvironmentPage.cs b/src/App/Pages/EnvironmentPage.cs deleted file mode 100644 index 85304a0be..000000000 --- a/src/App/Pages/EnvironmentPage.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Bit.App.Utilities; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; - -namespace Bit.App.Pages -{ - public class EnvironmentPage : ExtendedContentPage - { - private IAppSettingsService _appSettings; - private IDeviceActionService _deviceActionService; - private IGoogleAnalyticsService _googleAnalyticsService; - - public EnvironmentPage() - : base(updateActivity: false, requireAuth: false) - { - _appSettings = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell BaseUrlCell { get; set; } - public FormEntryCell WebVaultUrlCell { get; set; } - public FormEntryCell ApiUrlCell { get; set; } - public FormEntryCell IdentityUrlCell { get; set; } - public FormEntryCell IconsUrlCell { get; set; } - public RedrawableStackLayout StackLayout { get; set; } - public Label SelfHostLabel { get; set; } - public Label CustomLabel { get; set; } - - private void Init() - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - - IconsUrlCell = new FormEntryCell(AppResources.IconsUrl, entryKeyboard: Keyboard.Url); - IconsUrlCell.Entry.Text = _appSettings.IconsUrl; - - IdentityUrlCell = new FormEntryCell(AppResources.IdentityUrl, nextElement: IconsUrlCell.Entry, - entryKeyboard: Keyboard.Url); - IdentityUrlCell.Entry.Text = _appSettings.IdentityUrl; - - ApiUrlCell = new FormEntryCell(AppResources.ApiUrl, nextElement: IdentityUrlCell.Entry, - entryKeyboard: Keyboard.Url); - ApiUrlCell.Entry.Text = _appSettings.ApiUrl; - - WebVaultUrlCell = new FormEntryCell(AppResources.WebVaultUrl, nextElement: ApiUrlCell.Entry, - entryKeyboard: Keyboard.Url); - WebVaultUrlCell.Entry.Text = _appSettings.WebVaultUrl; - - BaseUrlCell = new FormEntryCell(AppResources.ServerUrl, nextElement: WebVaultUrlCell.Entry, - entryKeyboard: Keyboard.Url); - BaseUrlCell.Entry.Text = _appSettings.BaseUrl; - - var table = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(AppResources.SelfHostedEnvironment) - { - BaseUrlCell - } - } - }; - - SelfHostLabel = new Label - { - Text = AppResources.SelfHostedEnvironmentFooter, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 5) - }; - - var table2 = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(AppResources.CustomEnvironment) - { - WebVaultUrlCell, - ApiUrlCell, - IdentityUrlCell, - IconsUrlCell - } - } - }; - - CustomLabel = new Label - { - Text = AppResources.CustomEnvironmentFooter, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 5) - }; - - StackLayout = new RedrawableStackLayout - { - Children = { table, SelfHostLabel, table2, CustomLabel }, - Spacing = 0 - }; - - var scrollView = new ScrollView - { - Content = StackLayout - }; - - var toolbarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => await SaveAsync(), - ToolbarItemOrder.Default, 0); - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = table2.RowHeight = -1; - table.EstimatedRowHeight = table2.EstimatedRowHeight = 70; - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close, () => - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", false); - })); - } - - ToolbarItems.Add(toolbarItem); - Title = AppResources.Settings; - Content = scrollView; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - BaseUrlCell.InitEvents(); - IconsUrlCell.InitEvents(); - IdentityUrlCell.InitEvents(); - ApiUrlCell.InitEvents(); - WebVaultUrlCell.InitEvents(); - BaseUrlCell.Entry.FocusWithDelay(); - } - protected override void OnDisappearing() - { - base.OnDisappearing(); - BaseUrlCell.Dispose(); - IconsUrlCell.Dispose(); - IdentityUrlCell.Dispose(); - ApiUrlCell.Dispose(); - WebVaultUrlCell.Dispose(); - } - - private async Task SaveAsync() - { - Uri result; - - if(!string.IsNullOrWhiteSpace(BaseUrlCell.Entry.Text)) - { - BaseUrlCell.Entry.Text = FixUrl(BaseUrlCell.Entry.Text); - if(!Uri.TryCreate(BaseUrlCell.Entry.Text, UriKind.Absolute, out result)) - { - await DisplayAlert(null, string.Format(AppResources.FormattedIncorrectly, AppResources.ServerUrl), - AppResources.Ok); - return; - } - } - else - { - BaseUrlCell.Entry.Text = null; - } - - if(!string.IsNullOrWhiteSpace(WebVaultUrlCell.Entry.Text)) - { - WebVaultUrlCell.Entry.Text = FixUrl(WebVaultUrlCell.Entry.Text); - if(!Uri.TryCreate(WebVaultUrlCell.Entry.Text, UriKind.Absolute, out result)) - { - await DisplayAlert(null, string.Format(AppResources.FormattedIncorrectly, AppResources.WebVaultUrl), - AppResources.Ok); - return; - } - } - else - { - WebVaultUrlCell.Entry.Text = null; - } - - if(!string.IsNullOrWhiteSpace(ApiUrlCell.Entry.Text)) - { - ApiUrlCell.Entry.Text = FixUrl(ApiUrlCell.Entry.Text); - if(!Uri.TryCreate(ApiUrlCell.Entry.Text, UriKind.Absolute, out result)) - { - await DisplayAlert(null, string.Format(AppResources.FormattedIncorrectly, AppResources.ApiUrl), - AppResources.Ok); - return; - } - } - else - { - ApiUrlCell.Entry.Text = null; - } - - if(!string.IsNullOrWhiteSpace(IdentityUrlCell.Entry.Text)) - { - IdentityUrlCell.Entry.Text = FixUrl(IdentityUrlCell.Entry.Text); - if(!Uri.TryCreate(IdentityUrlCell.Entry.Text, UriKind.Absolute, out result)) - { - await DisplayAlert(null, string.Format(AppResources.FormattedIncorrectly, AppResources.IdentityUrl), - AppResources.Ok); - return; - } - } - else - { - IdentityUrlCell.Entry.Text = null; - } - - if(!string.IsNullOrWhiteSpace(IconsUrlCell.Entry.Text)) - { - IconsUrlCell.Entry.Text = FixUrl(IconsUrlCell.Entry.Text); - if(!Uri.TryCreate(IconsUrlCell.Entry.Text, UriKind.Absolute, out result)) - { - await DisplayAlert(null, string.Format(AppResources.FormattedIncorrectly, AppResources.IconsUrl), - AppResources.Ok); - return; - } - } - else - { - IconsUrlCell.Entry.Text = null; - } - - _appSettings.BaseUrl = BaseUrlCell.Entry.Text; - _appSettings.IconsUrl = IconsUrlCell.Entry.Text; - _appSettings.IdentityUrl = IdentityUrlCell.Entry.Text; - _appSettings.ApiUrl = ApiUrlCell.Entry.Text; - _appSettings.WebVaultUrl = WebVaultUrlCell.Entry.Text; - _deviceActionService.Toast(AppResources.EnvironmentSaved); - _googleAnalyticsService.TrackAppEvent("SetEnvironmentUrls"); - await Navigation.PopForDeviceAsync(); - } - - private string FixUrl(string url) - { - url = url.TrimEnd('/'); - if(!url.StartsWith("http://") && !url.StartsWith("https://")) - { - url = $"https://{url}"; - } - return url; - } - - private class FormTableView : ExtendedTableView - { - public FormTableView(EnvironmentPage page) - { - Intent = TableIntent.Settings; - EnableScrolling = false; - HasUnevenRows = true; - EnableSelection = true; - VerticalOptions = LayoutOptions.Start; - NoFooter = true; - WrappingStackLayout = () => page.StackLayout; - } - } - } -} diff --git a/src/App/Pages/Generator/GeneratorHistoryPage.xaml b/src/App/Pages/Generator/GeneratorHistoryPage.xaml new file mode 100644 index 000000000..7bd473634 --- /dev/null +++ b/src/App/Pages/Generator/GeneratorHistoryPage.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs b/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs new file mode 100644 index 000000000..914afa87e --- /dev/null +++ b/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs @@ -0,0 +1,30 @@ +using System; + +namespace Bit.App.Pages +{ + public partial class GeneratorHistoryPage : BaseContentPage + { + private GeneratorHistoryPageViewModel _vm; + + public GeneratorHistoryPage() + { + InitializeComponent(); + SetActivityIndicator(); + _vm = BindingContext as GeneratorHistoryPageViewModel; + _vm.Page = this; + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, true, async () => { + await _vm.InitAsync(); + }); + } + + private async void Clear_Clicked(object sender, EventArgs e) + { + await _vm.ClearAsync(); + } + } +} diff --git a/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs new file mode 100644 index 000000000..ad7f59195 --- /dev/null +++ b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs @@ -0,0 +1,58 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class GeneratorHistoryPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IPasswordGenerationService _passwordGenerationService; + + private bool _showNoData; + + public GeneratorHistoryPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _passwordGenerationService = ServiceContainer.Resolve( + "passwordGenerationService"); + + PageTitle = AppResources.PasswordHistory; + History = new ExtendedObservableCollection(); + CopyCommand = new Command(CopyAsync); + } + + public Command CopyCommand { get; set; } + public ExtendedObservableCollection History { get; set; } + + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } + + public async Task InitAsync() + { + var history = await _passwordGenerationService.GetHistoryAsync(); + History.ResetWithRange(history ?? new List()); + ShowNoData = History.Count == 0; + } + + public async Task ClearAsync() + { + History.ResetWithRange(new List()); + await _passwordGenerationService.ClearAsync(); + } + + private async void CopyAsync(GeneratedPasswordHistory ph) + { + await _platformUtilsService.CopyToClipboardAsync(ph.Password); + _platformUtilsService.ShowToast("info", null, + string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); + } + } +} diff --git a/src/App/Pages/Generator/GeneratorPage.xaml b/src/App/Pages/Generator/GeneratorPage.xaml new file mode 100644 index 000000000..609a31874 --- /dev/null +++ b/src/App/Pages/Generator/GeneratorPage.xaml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Generator/GeneratorPage.xaml.cs b/src/App/Pages/Generator/GeneratorPage.xaml.cs new file mode 100644 index 000000000..d5b9495c1 --- /dev/null +++ b/src/App/Pages/Generator/GeneratorPage.xaml.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class GeneratorPage : BaseContentPage + { + private GeneratorPageViewModel _vm; + private readonly bool _fromTabPage; + private readonly Action _selectAction; + + public GeneratorPage(bool fromTabPage, Action selectAction = null) + { + InitializeComponent(); + _vm = BindingContext as GeneratorPageViewModel; + _vm.Page = this; + _fromTabPage = fromTabPage; + _selectAction = selectAction; + if(selectAction == null) + { + ToolbarItems.Remove(_selectItem); + } + } + + public async Task InitAsync() + { + await _vm.InitAsync(); + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + if(!_fromTabPage) + { + await InitAsync(); + } + } + + private async void Regenerate_Clicked(object sender, EventArgs e) + { + await _vm.RegenerateAsync(); + } + + private async void Copy_Clicked(object sender, EventArgs e) + { + await _vm.CopyAsync(); + } + + private void Select_Clicked(object sender, EventArgs e) + { + _selectAction?.Invoke(_vm.Password); + } + + private async void History_Clicked(object sender, EventArgs e) + { + var page = new GeneratorHistoryPage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } +} diff --git a/src/App/Pages/Generator/GeneratorPageViewModel.cs b/src/App/Pages/Generator/GeneratorPageViewModel.cs new file mode 100644 index 000000000..f12fe0a34 --- /dev/null +++ b/src/App/Pages/Generator/GeneratorPageViewModel.cs @@ -0,0 +1,274 @@ +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class GeneratorPageViewModel : BaseViewModel + { + private readonly IPasswordGenerationService _passwordGenerationService; + private readonly IPlatformUtilsService _platformUtilsService; + + private PasswordGenerationOptions _options; + private string _password; + private bool _isPassword; + private bool _uppercase; + private bool _lowercase; + private bool _number; + private bool _special; + private bool _avoidAmbiguous; + private int _minNumber; + private int _minSpecial; + private int _length = 5; + private int _numWords = 3; + private string _wordSeparator; + private int _typeSelectedIndex; + private bool _doneIniting; + + public GeneratorPageViewModel() + { + _passwordGenerationService = ServiceContainer.Resolve( + "passwordGenerationService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + PageTitle = AppResources.PasswordGenerator; + TypeOptions = new List { AppResources.Password, AppResources.Passphrase }; + } + + public List TypeOptions { get; set; } + + public string Password + { + get => _password; + set => SetProperty(ref _password, value, + additionalPropertyNames: new string[] + { + nameof(ColoredPassword) + }); + } + + public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Password); + + public bool IsPassword + { + get => _isPassword; + set => SetProperty(ref _isPassword, value); + } + + public int Length + { + get => _length; + set + { + if(SetProperty(ref _length, value)) + { + _options.Length = value; + var task = SaveOptionsAsync(); + } + } + } + + public bool Uppercase + { + get => _uppercase; + set + { + if(SetProperty(ref _uppercase, value)) + { + _options.Uppercase = value; + var task = SaveOptionsAsync(); + } + } + } + + public bool Lowercase + { + get => _lowercase; + set + { + if(SetProperty(ref _lowercase, value)) + { + _options.Lowercase = value; + var task = SaveOptionsAsync(); + } + } + } + + public bool Number + { + get => _number; + set + { + if(SetProperty(ref _number, value)) + { + _options.Number = value; + var task = SaveOptionsAsync(); + } + } + } + + public bool Special + { + get => _special; + set + { + if(SetProperty(ref _special, value)) + { + _options.Special = value; + var task = SaveOptionsAsync(); + } + } + } + + public bool AvoidAmbiguous + { + get => _avoidAmbiguous; + set + { + if(SetProperty(ref _avoidAmbiguous, value)) + { + _options.Ambiguous = !value; + var task = SaveOptionsAsync(); + } + } + } + + public int MinNumber + { + get => _minNumber; + set + { + if(SetProperty(ref _minNumber, value)) + { + _options.MinNumber = value; + var task = SaveOptionsAsync(); + } + } + } + + public int MinSpecial + { + get => _minSpecial; + set + { + if(SetProperty(ref _minSpecial, value)) + { + _options.MinSpecial = value; + var task = SaveOptionsAsync(); + } + } + } + + public int NumWords + { + get => _numWords; + set + { + if(SetProperty(ref _numWords, value)) + { + _options.NumWords = value; + var task = SaveOptionsAsync(); + } + } + } + + public string WordSeparator + { + get => _wordSeparator; + set + { + var val = value.Trim(); + if(SetProperty(ref _wordSeparator, val)) + { + _options.WordSeparator = val; + var task = SaveOptionsAsync(); + } + } + } + + public int TypeSelectedIndex + { + get => _typeSelectedIndex; + set + { + if(SetProperty(ref _typeSelectedIndex, value)) + { + IsPassword = value == 0; + var task = SaveOptionsAsync(); + } + } + } + + public async Task InitAsync() + { + _options = await _passwordGenerationService.GetOptionsAsync(); + LoadFromOptions(); + Password = await _passwordGenerationService.GeneratePasswordAsync(_options); + await _passwordGenerationService.AddHistoryAsync(Password); + _doneIniting = true; + } + + public async Task RegenerateAsync() + { + Password = await _passwordGenerationService.GeneratePasswordAsync(_options); + await _passwordGenerationService.AddHistoryAsync(Password); + } + + public async Task SaveOptionsAsync(bool regenerate = true) + { + if(!_doneIniting) + { + return; + } + SetOptions(); + _passwordGenerationService.NormalizeOptions(_options); + await _passwordGenerationService.SaveOptionsAsync(_options); + LoadFromOptions(); + if(regenerate) + { + await RegenerateAsync(); + } + } + + public async Task CopyAsync() + { + await _platformUtilsService.CopyToClipboardAsync(Password); + _platformUtilsService.ShowToast("success", null, AppResources.CopiedPassword); + } + + private void LoadFromOptions() + { + AvoidAmbiguous = !_options.Ambiguous.GetValueOrDefault(); + TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0; + IsPassword = TypeSelectedIndex == 0; + MinNumber = _options.MinNumber.GetValueOrDefault(); + MinSpecial = _options.MinSpecial.GetValueOrDefault(); + Special = _options.Special.GetValueOrDefault(); + Number = _options.Number.GetValueOrDefault(); + NumWords = _options.NumWords.GetValueOrDefault(); + WordSeparator = _options.WordSeparator; + Uppercase = _options.Uppercase.GetValueOrDefault(); + Lowercase = _options.Lowercase.GetValueOrDefault(); + Length = _options.Length.GetValueOrDefault(5); + } + + private void SetOptions() + { + _options.Ambiguous = !AvoidAmbiguous; + _options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password"; + _options.MinNumber = MinNumber; + _options.MinSpecial = MinSpecial; + _options.Special = Special; + _options.NumWords = NumWords; + _options.Number = Number; + _options.WordSeparator = WordSeparator; + _options.Uppercase = Uppercase; + _options.Lowercase = Lowercase; + _options.Length = Length; + } + } +} diff --git a/src/App/Pages/HomePage.cs b/src/App/Pages/HomePage.cs deleted file mode 100644 index 6e95f6dfb..000000000 --- a/src/App/Pages/HomePage.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Plugin.Settings.Abstractions; -using Bit.App.Controls; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class HomePage : ExtendedContentPage - { - private readonly IAuthService _authService; - private readonly ISettings _settings; - private readonly IDeviceActionService _deviceActionService; - private DateTime? _lastAction; - - public HomePage() - : base(updateActivity: false, requireAuth: false) - { - _authService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - - Init(); - } - - public void Init() - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", false); - - var settingsButton = new ExtendedButton - { - Image = "cog.png", - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - WidthRequest = 25, - HeightRequest = 25, - BackgroundColor = Color.Transparent, - Margin = new Thickness(-20, -30, 0, 0), - Command = new Command(async () => await SettingsAsync()) - }; - - var logo = new CachedImage - { - Source = "logo.png", - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center, - WidthRequest = 282, - Margin = new Thickness(0, 30, 0, 0), - HeightRequest = 44 - }; - - var message = new Label - { - Text = AppResources.LoginOrCreateNewAccount, - VerticalOptions = LayoutOptions.StartAndExpand, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), - TextColor = Color.FromHex("333333") - }; - - var createAccountButton = new ExtendedButton - { - Text = AppResources.CreateAccount, - Command = new Command(async () => await RegisterAsync()), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"], - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Button)) - }; - - var loginButton = new ExtendedButton - { - Text = AppResources.LogIn, - Command = new Command(async () => await LoginAsync()), - VerticalOptions = LayoutOptions.End, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - HorizontalOptions = LayoutOptions.Fill, - BackgroundColor = Color.Transparent, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Button)) - }; - - var buttonStackLayout = new StackLayout - { - Padding = new Thickness(30, 40), - Spacing = 10, - Children = { settingsButton, logo, message, createAccountButton, loginButton } - }; - - Title = AppResources.Bitwarden; - NavigationPage.SetHasNavigationBar(this, false); - Content = new ScrollView { Content = buttonStackLayout }; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - MessagingCenter.Send(Application.Current, "ShowStatusBar", false); - } - - public async Task LoginAsync() - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - await Navigation.PushForDeviceAsync(new LoginPage()); - } - - public async Task RegisterAsync() - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - await Navigation.PushForDeviceAsync(new RegisterPage(this)); - } - - public async Task DismissRegisterAndLoginAsync(string email) - { - await Navigation.PopForDeviceAsync(); - await Navigation.PushForDeviceAsync(new LoginPage(email)); - _deviceActionService.Toast(AppResources.AccountCreated); - } - - public async Task SettingsAsync() - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - await Navigation.PushForDeviceAsync(new EnvironmentPage()); - } - } -} diff --git a/src/App/Pages/Lock/BaseLockPage.cs b/src/App/Pages/Lock/BaseLockPage.cs deleted file mode 100644 index 0dcbbc35f..000000000 --- a/src/App/Pages/Lock/BaseLockPage.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Threading.Tasks; -using Bit.App.Controls; -using Bit.App.Resources; -using XLabs.Ioc; -using Bit.App.Abstractions; - -namespace Bit.App.Pages -{ - public class BaseLockPage : ExtendedContentPage - { - private readonly IDeviceActionService _deviceActionService; - - public BaseLockPage() - : base(false, false) - { - AuthService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - } - - protected IAuthService AuthService { get; set; } - - protected override bool OnBackButtonPressed() - { - _deviceActionService.Background(); - return true; - } - - protected async Task LogoutAsync() - { - var confirmed = await DisplayAlert(null, AppResources.LogoutConfirmation, AppResources.Yes, AppResources.Cancel); - if(!confirmed) - { - return; - } - AuthService.LogOut(); - } - } -} diff --git a/src/App/Pages/Lock/LockFingerprintPage.cs b/src/App/Pages/Lock/LockFingerprintPage.cs deleted file mode 100644 index 6e0ae7b42..000000000 --- a/src/App/Pages/Lock/LockFingerprintPage.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Plugin.Fingerprint.Abstractions; -using Plugin.Settings.Abstractions; -using Bit.App.Abstractions; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class LockFingerprintPage : BaseLockPage - { - private readonly IFingerprint _fingerprint; - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettings; - private readonly IDeviceInfoService _deviceInfoService; - private readonly bool _checkFingerprintImmediately; - private DateTime? _lastAction; - - public LockFingerprintPage(bool checkFingerprintImmediately) - { - _checkFingerprintImmediately = checkFingerprintImmediately; - _fingerprint = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _appSettings = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - - Init(); - } - - public void Init() - { - var biometricIcon = Helpers.OnPlatform( - iOS: _deviceInfoService.HasFaceIdSupport ? "smile.png" : "fingerprint.png", - Android: "fingerprint.png", - Windows: "smile.png"); - var biometricText = Helpers.OnPlatform( - iOS: _deviceInfoService.HasFaceIdSupport ? - AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock, - Android: AppResources.UseFingerprintToUnlock, - Windows: AppResources.UseWindowsHelloToUnlock); - var biometricTitle = Helpers.OnPlatform( - iOS: _deviceInfoService.HasFaceIdSupport ? - AppResources.VerifyFaceID : AppResources.VerifyFingerprint, - Android: AppResources.VerifyFingerprint, - Windows: AppResources.VerifyWindowsHello); - - - var fingerprintIcon = new ExtendedButton - { - Image = biometricIcon, - BackgroundColor = Color.Transparent, - Command = new Command(async () => await CheckFingerprintAsync()), - VerticalOptions = LayoutOptions.CenterAndExpand, - Margin = new Thickness(0, 0, 0, 15) - }; - - var fingerprintButton = new ExtendedButton - { - Text = biometricText, - Command = new Command(async () => await CheckFingerprintAsync()), - VerticalOptions = LayoutOptions.EndAndExpand, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - - var logoutButton = new ExtendedButton - { - Text = AppResources.LogOut, - Command = new Command(async () => await LogoutAsync()), - VerticalOptions = LayoutOptions.End, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - BackgroundColor = Color.Transparent, - Uppercase = false - }; - - var stackLayout = new StackLayout - { - Padding = new Thickness(30, 40), - Spacing = 10, - Children = { fingerprintIcon, fingerprintButton, logoutButton } - }; - - Title = biometricTitle; - Content = stackLayout; - } - - protected override async void OnAppearing() - { - base.OnAppearing(); - - if(_checkFingerprintImmediately) - { - await Task.Delay(Device.RuntimePlatform == Device.Android ? 500 : 200); - await CheckFingerprintAsync(); - } - } - - public async Task CheckFingerprintAsync() - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - var direction = _deviceInfoService.HasFaceIdSupport ? - AppResources.FaceIDDirection : AppResources.FingerprintDirection; - - var fingerprintRequest = new AuthenticationRequestConfiguration(direction) - { - AllowAlternativeAuthentication = true, - CancelTitle = AppResources.Cancel, - FallbackTitle = AppResources.LogOut - }; - var result = await _fingerprint.AuthenticateAsync(fingerprintRequest); - if(result.Authenticated) - { - _appSettings.Locked = false; - if(Navigation.ModalStack.Count > 0) - { - await Navigation.PopModalAsync(); - } - } - else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested) - { - AuthService.LogOut(); - } - } - } -} diff --git a/src/App/Pages/Lock/LockPasswordPage.cs b/src/App/Pages/Lock/LockPasswordPage.cs deleted file mode 100644 index 78aaf735a..000000000 --- a/src/App/Pages/Lock/LockPasswordPage.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Controls; -using System.Linq; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class LockPasswordPage : BaseLockPage - { - private readonly IAuthService _authService; - private readonly IAppSettingsService _appSettingsService; - private readonly ICryptoService _cryptoService; - private DateTime? _lastAction; - - public LockPasswordPage() - { - _authService = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - _cryptoService = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell PasswordCell { get; set; } - - public void Init() - { - var padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(15, 8), - Windows: new Thickness(10, 8)); - - PasswordCell = new FormEntryCell(AppResources.MasterPassword, isPassword: true, - useLabelAsPlaceholder: true, imageSource: "lock.png", containerPadding: padding); - - PasswordCell.Entry.TargetReturnType = Enums.ReturnType.Go; - - var table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = false, - HasUnevenRows = true, - EnableSelection = false, - VerticalOptions = LayoutOptions.Start, - NoFooter = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - PasswordCell - } - } - }; - - var logoutButton = new ExtendedButton - { - Text = AppResources.LogOut, - Command = new Command(async () => await LogoutAsync()), - VerticalOptions = LayoutOptions.End, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - BackgroundColor = Color.Transparent, - Uppercase = false - }; - - var stackLayout = new RedrawableStackLayout - { - Spacing = 10, - Children = { table, logoutButton } - }; - - table.WrappingStackLayout = () => stackLayout; - - var scrollView = new ScrollView { Content = stackLayout }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 70; - } - - var loginToolbarItem = new ToolbarItem(AppResources.Submit, Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - await CheckPasswordAsync(); - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(loginToolbarItem); - Title = AppResources.VerifyMasterPassword; - Content = scrollView; - } - - private async void Entry_Completed(object sender, EventArgs e) - { - await CheckPasswordAsync(); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - PasswordCell.InitEvents(); - PasswordCell.Entry.Completed += Entry_Completed; - - if(Device.RuntimePlatform == Device.Android) - { - Task.Run(async () => - { - for(int i = 0; i < 5; i++) - { - if(!PasswordCell.Entry.IsFocused) - { - Device.BeginInvokeOnMainThread(() => PasswordCell.Entry.FocusWithDelay()); - } - else - { - break; - } - - await Task.Delay(1000); - } - }); - } - else - { - PasswordCell.Entry.Focus(); - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - PasswordCell.Dispose(); - PasswordCell.Entry.Completed -= Entry_Completed; - } - - protected async Task CheckPasswordAsync() - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(string.IsNullOrWhiteSpace(PasswordCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.MasterPassword), AppResources.Ok); - return; - } - - var key = _cryptoService.MakeKeyFromPassword(PasswordCell.Entry.Text, _authService.Email, - _authService.Kdf, _authService.KdfIterations); - if(key.Key.SequenceEqual(_cryptoService.Key.Key)) - { - _appSettingsService.Locked = false; - if(Navigation.ModalStack.Count > 0) - { - await Navigation.PopModalAsync(); - } - } - else - { - // TODO: keep track of invalid attempts and logout? - - await DisplayAlert(null, AppResources.InvalidMasterPassword, AppResources.Ok); - PasswordCell.Entry.Text = string.Empty; - PasswordCell.Entry.Focus(); - } - } - } -} diff --git a/src/App/Pages/Lock/LockPinPage.cs b/src/App/Pages/Lock/LockPinPage.cs deleted file mode 100644 index df21f499e..000000000 --- a/src/App/Pages/Lock/LockPinPage.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Models.Page; -using Bit.App.Controls; -using System.Threading.Tasks; - -namespace Bit.App.Pages -{ - public class LockPinPage : BaseLockPage - { - private readonly IAuthService _authService; - private readonly IAppSettingsService _appSettingsService; - private TapGestureRecognizer _tgr; - private DateTime? _lastAction; - - public LockPinPage() - { - _authService = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - - Init(); - } - - public PinPageModel Model { get; set; } = new PinPageModel(); - public PinControl PinControl { get; set; } - - public void Init() - { - var instructionLabel = new Label - { - Text = AppResources.EnterPIN, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - HorizontalTextAlignment = TextAlignment.Center, - Style = (Style)Application.Current.Resources["text-muted"] - }; - - PinControl = new PinControl(); - PinControl.Label.SetBinding(Label.TextProperty, nameof(PinPageModel.LabelText)); - PinControl.Entry.SetBinding(Entry.TextProperty, nameof(PinPageModel.PIN)); - - var logoutButton = new ExtendedButton - { - Text = AppResources.LogOut, - Command = new Command(async () => await LogoutAsync()), - VerticalOptions = LayoutOptions.End, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - BackgroundColor = Color.Transparent, - Uppercase = false - }; - - var stackLayout = new StackLayout - { - Padding = new Thickness(30, 40), - Spacing = 20, - Children = { PinControl.Label, instructionLabel, logoutButton, PinControl.Entry } - }; - - _tgr = new TapGestureRecognizer(); - PinControl.Label.GestureRecognizers.Add(_tgr); - instructionLabel.GestureRecognizers.Add(_tgr); - - Title = AppResources.VerifyPIN; - Content = stackLayout; - Content.GestureRecognizers.Add(_tgr); - BindingContext = Model; - } - - private void Tgr_Tapped(object sender, EventArgs e) - { - PinControl.Entry.Focus(); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - _tgr.Tapped += Tgr_Tapped; - PinControl.OnPinEntered += PinEntered; - PinControl.InitEvents(); - - if(Device.RuntimePlatform == Device.Android) - { - Task.Run(async () => - { - for(int i = 0; i < 5; i++) - { - await Task.Delay(1000); - if(!PinControl.Entry.IsFocused) - { - Device.BeginInvokeOnMainThread(() => PinControl.Entry.Focus()); - } - else - { - break; - } - } - }); - } - else - { - PinControl.Entry.Focus(); - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - _tgr.Tapped -= Tgr_Tapped; - PinControl.OnPinEntered -= PinEntered; - PinControl.Dispose(); - } - - protected async void PinEntered(object sender, EventArgs args) - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(Model.PIN == _authService.PIN) - { - _appSettingsService.Locked = false; - _appSettingsService.FailedPinAttempts = 0; - PinControl.Entry.Unfocus(); - if(Navigation.ModalStack.Count > 0) - { - await Navigation.PopModalAsync(); - } - } - else - { - _appSettingsService.FailedPinAttempts++; - if(_appSettingsService.FailedPinAttempts >= 5) - { - PinControl.Entry.Unfocus(); - AuthService.LogOut(); - return; - } - - await DisplayAlert(null, AppResources.InvalidPIN, AppResources.Ok); - Model.PIN = string.Empty; - PinControl.Entry.Focus(); - } - } - } -} diff --git a/src/App/Pages/LoginPage.cs b/src/App/Pages/LoginPage.cs deleted file mode 100644 index eb49ad49d..000000000 --- a/src/App/Pages/LoginPage.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Plugin.Settings.Abstractions; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class LoginPage : ExtendedContentPage - { - private IAuthService _authService; - private ISyncService _syncService; - private IDeviceActionService _deviceActionService; - private ISettings _settings; - private IGoogleAnalyticsService _googleAnalyticsService; - private IPushNotificationService _pushNotification; - private readonly string _email; - - public LoginPage(string email = null) - : base(updateActivity: false, requireAuth: false) - { - _email = email; - _authService = Resolver.Resolve(); - _syncService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _pushNotification = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell PasswordCell { get; set; } - public FormEntryCell EmailCell { get; set; } - - private void Init() - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - - var padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(15, 8), - Windows: new Thickness(10, 8)); - - PasswordCell = new FormEntryCell(AppResources.MasterPassword, isPassword: true, - useLabelAsPlaceholder: true, imageSource: "lock.png", containerPadding: padding); - EmailCell = new FormEntryCell(AppResources.EmailAddress, nextElement: PasswordCell.Entry, - entryKeyboard: Keyboard.Email, useLabelAsPlaceholder: true, imageSource: "envelope.png", - containerPadding: padding); - - var lastLoginEmail = _settings.GetValueOrDefault(Constants.LastLoginEmail, string.Empty); - if(!string.IsNullOrWhiteSpace(_email)) - { - EmailCell.Entry.Text = _email; - } - else if(!string.IsNullOrWhiteSpace(lastLoginEmail)) - { - EmailCell.Entry.Text = lastLoginEmail; - } - - PasswordCell.Entry.TargetReturnType = Enums.ReturnType.Go; - - var table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = false, - HasUnevenRows = true, - EnableSelection = true, - NoFooter = true, - VerticalOptions = LayoutOptions.Start, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - EmailCell, - PasswordCell - } - } - }; - - var forgotPasswordButton = new ExtendedButton - { - Text = AppResources.GetPasswordHint, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Command = new Command(async () => await ForgotPasswordAsync()), - VerticalOptions = LayoutOptions.End, - Uppercase = false, - BackgroundColor = Color.Transparent - }; - - var layout = new RedrawableStackLayout - { - Children = { table, forgotPasswordButton }, - Spacing = 10 - }; - - table.WrappingStackLayout = () => layout; - - var scrollView = new ScrollView { Content = layout }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 70; - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel, () => - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", false); - })); - } - - var loginToolbarItem = new ToolbarItem(AppResources.LogIn, Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - await LogIn(); - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(loginToolbarItem); - Title = AppResources.Bitwarden; - Content = scrollView; - NavigationPage.SetBackButtonTitle(this, AppResources.LogIn); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - PasswordCell.InitEvents(); - EmailCell.InitEvents(); - - PasswordCell.Entry.Completed += Entry_Completed; - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - - if(string.IsNullOrWhiteSpace(_email)) - { - if(!string.IsNullOrWhiteSpace(EmailCell.Entry.Text)) - { - PasswordCell.Entry.FocusWithDelay(); - } - else - { - EmailCell.Entry.FocusWithDelay(); - } - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - PasswordCell.Dispose(); - EmailCell.Dispose(); - PasswordCell.Entry.Completed -= Entry_Completed; - } - - private async void Entry_Completed(object sender, EventArgs e) - { - await LogIn(); - } - - private async Task ForgotPasswordAsync() - { - await Navigation.PushAsync(new PasswordHintPage()); - } - - private async Task LogIn() - { - if(string.IsNullOrWhiteSpace(EmailCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.EmailAddress), AppResources.Ok); - return; - } - - if(string.IsNullOrWhiteSpace(PasswordCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.MasterPassword), AppResources.Ok); - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); - var result = await _authService.TokenPostAsync(EmailCell.Entry.Text, PasswordCell.Entry.Text); - await _deviceActionService.HideLoadingAsync(); - - if(!result.Success) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, result.ErrorMessage, AppResources.Ok); - return; - } - - PasswordCell.Entry.Text = string.Empty; - - if(result.TwoFactorRequired) - { - _googleAnalyticsService.TrackAppEvent("LoggedIn To Two-step"); - await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result)); - return; - } - - _googleAnalyticsService.TrackAppEvent("LoggedIn"); - - var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); - Application.Current.MainPage = new MainPage(); - } - } -} diff --git a/src/App/Pages/LoginTwoFactorPage.cs b/src/App/Pages/LoginTwoFactorPage.cs deleted file mode 100644 index ad7135580..000000000 --- a/src/App/Pages/LoginTwoFactorPage.cs +++ /dev/null @@ -1,614 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Bit.App.Models; -using Bit.App.Utilities; -using Bit.App.Enums; -using System.Collections.Generic; -using System.Net; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class LoginTwoFactorPage : ExtendedContentPage - { - private DateTime? _lastAction; - private IAuthService _authService; - private ISyncService _syncService; - private IDeviceInfoService _deviceInfoService; - private IDeviceActionService _deviceActionService; - private IGoogleAnalyticsService _googleAnalyticsService; - private ITwoFactorApiRepository _twoFactorApiRepository; - private IPushNotificationService _pushNotification; - private IAppSettingsService _appSettingsService; - private readonly string _email; - private readonly string _masterPasswordHash; - private readonly SymmetricCryptoKey _key; - private readonly Dictionary> _providers; - private TwoFactorProviderType? _providerType; - private readonly FullLoginResult _result; - private readonly string _duoOrgTitle; - - public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null) - : base(updateActivity: false, requireAuth: false) - { - _duoOrgTitle = $"Duo ({AppResources.Organization})"; - _deviceInfoService = Resolver.Resolve(); - - _email = email; - _result = result; - _masterPasswordHash = result.MasterPasswordHash; - _key = result.Key; - _providers = result.TwoFactorProviders; - _providerType = type ?? GetDefaultProvider(); - - _deviceActionService = Resolver.Resolve(); - _authService = Resolver.Resolve(); - _syncService = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _twoFactorApiRepository = Resolver.Resolve(); - _pushNotification = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell TokenCell { get; set; } - public ExtendedSwitchCell RememberCell { get; set; } - - private void Init() - { - SubscribeYubiKey(true); - if(_providers.Count > 1) - { - var sendEmailTask = SendEmailAsync(false); - } - - ToolbarItems.Clear(); - var scrollView = new ScrollView(); - - var anotherMethodButton = new ExtendedButton - { - Text = AppResources.UseAnotherTwoStepMethod, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Margin = new Thickness(15, 0, 15, 25), - Command = new Command(() => AnotherMethodAsync()), - Uppercase = false, - BackgroundColor = Color.Transparent, - VerticalOptions = LayoutOptions.Start - }; - - var instruction = new Label - { - LineBreakMode = LineBreakMode.WordWrap, - Margin = new Thickness(15), - HorizontalTextAlignment = TextAlignment.Center - }; - - RememberCell = new ExtendedSwitchCell - { - Text = AppResources.RememberMe, - On = false - }; - - var continueToolbarItem = new ToolbarItem(AppResources.Continue, - Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - var token = TokenCell?.Entry.Text.Trim().Replace(" ", ""); - await LogInAsync(token); - }, ToolbarItemOrder.Default, 0); - - if(!_providerType.HasValue) - { - instruction.Text = AppResources.NoTwoStepAvailable; - - var layout = new StackLayout - { - Children = { instruction, anotherMethodButton }, - Spacing = 0 - }; - - scrollView.Content = layout; - - Title = AppResources.LoginUnavailable; - Content = scrollView; - } - else if(_providerType.Value == TwoFactorProviderType.Authenticator || - _providerType.Value == TwoFactorProviderType.Email) - { - var padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(15, 8), - Windows: new Thickness(10, 8)); - - TokenCell = new FormEntryCell(AppResources.VerificationCode, useLabelAsPlaceholder: true, - imageSource: "lock", containerPadding: padding); - - TokenCell.Entry.Keyboard = Keyboard.Numeric; - TokenCell.Entry.TargetReturnType = Enums.ReturnType.Go; - - var table = new TwoFactorTable( - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - TokenCell, - RememberCell - }); - - var layout = new RedrawableStackLayout - { - Children = { instruction, table }, - Spacing = 0 - }; - - table.WrappingStackLayout = () => layout; - scrollView.Content = layout; - - switch(_providerType.Value) - { - case TwoFactorProviderType.Authenticator: - instruction.Text = AppResources.EnterVerificationCodeApp; - layout.Children.Add(anotherMethodButton); - break; - case TwoFactorProviderType.Email: - var emailParams = _providers[TwoFactorProviderType.Email]; - var redactedEmail = emailParams["Email"].ToString(); - - instruction.Text = string.Format(AppResources.EnterVerificationCodeEmail, redactedEmail); - var resendEmailButton = new ExtendedButton - { - Text = AppResources.SendVerificationCodeAgain, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Margin = new Thickness(15, 0, 15, 0), - Command = new Command(async () => await SendEmailAsync(true)), - Uppercase = false, - BackgroundColor = Color.Transparent, - VerticalOptions = LayoutOptions.Start - }; - - layout.Children.Add(resendEmailButton); - layout.Children.Add(anotherMethodButton); - break; - default: - break; - } - - ToolbarItems.Add(continueToolbarItem); - Title = AppResources.VerificationCode; - - Content = scrollView; - TokenCell.Entry.FocusWithDelay(); - } - else if(_providerType == TwoFactorProviderType.Duo || - _providerType == TwoFactorProviderType.OrganizationDuo) - { - var duoParams = _providers[_providerType.Value]; - - var host = WebUtility.UrlEncode(duoParams["Host"].ToString()); - var req = WebUtility.UrlEncode(duoParams["Signature"].ToString()); - - var webVaultUrl = "https://vault.bitwarden.com"; - if(!string.IsNullOrWhiteSpace(_appSettingsService.BaseUrl)) - { - webVaultUrl = _appSettingsService.BaseUrl; - } - else if(!string.IsNullOrWhiteSpace(_appSettingsService.WebVaultUrl)) - { - webVaultUrl = _appSettingsService.WebVaultUrl; - } - - var webView = new HybridWebView - { - Uri = $"{webVaultUrl}/duo-connector.html?host={host}&request={req}", - HorizontalOptions = LayoutOptions.FillAndExpand, - VerticalOptions = LayoutOptions.FillAndExpand, - MinimumHeightRequest = 400 - }; - webView.RegisterAction(async (sig) => - { - await LogInAsync(sig); - }); - - var table = new TwoFactorTable( - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - RememberCell - }); - - var layout = new RedrawableStackLayout - { - Children = { webView, table, anotherMethodButton }, - Spacing = 0 - }; - - table.WrappingStackLayout = () => layout; - scrollView.Content = layout; - - Title = _providerType == TwoFactorProviderType.Duo ? "Duo" : _duoOrgTitle; - Content = scrollView; - } - else if(_providerType == TwoFactorProviderType.YubiKey) - { - instruction.Text = Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : - AppResources.YubiKeyInstruction; - - var image = new CachedImage - { - Source = "yubikey.png", - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - WidthRequest = 266, - HeightRequest = 160, - Margin = new Thickness(0, 0, 0, 25) - }; - - var section = new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - RememberCell - }; - - if(Device.RuntimePlatform != Device.iOS) - { - TokenCell = new FormEntryCell("", isPassword: true, imageSource: "lock", - useLabelAsPlaceholder: true); - TokenCell.Entry.TargetReturnType = Enums.ReturnType.Go; - section.Insert(0, TokenCell); - } - - var table = new TwoFactorTable(section); - var layout = new RedrawableStackLayout - { - Children = { instruction, image, table }, - Spacing = 0 - }; - - if(Device.RuntimePlatform == Device.iOS) - { - var tryAgainButton = new ExtendedButton - { - Text = AppResources.TryAgain, - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Margin = new Thickness(15, 0, 15, 0), - Command = new Command(() => ListenYubiKey(true, true)), - Uppercase = false, - BackgroundColor = Color.Transparent, - VerticalOptions = LayoutOptions.Start - }; - layout.Children.Add(tryAgainButton); - } - else - { - ToolbarItems.Add(continueToolbarItem); - } - - layout.Children.Add(anotherMethodButton); - - table.WrappingStackLayout = () => layout; - scrollView.Content = layout; - - Title = AppResources.YubiKeyTitle; - Content = scrollView; - } - } - - protected override void OnAppearing() - { - base.OnAppearing(); - ListenYubiKey(true); - - InitEvents(); - if(TokenCell == null && Device.RuntimePlatform == Device.Android) - { - _deviceActionService.DismissKeyboard(); - } - - if(TokenCell != null) - { - TokenCell.Entry.FocusWithDelay(); - } - } - - private void InitEvents() - { - if(TokenCell != null) - { - TokenCell.InitEvents(); - TokenCell.Entry.Completed += Entry_Completed; - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - ListenYubiKey(false); - - if(TokenCell != null) - { - TokenCell.Dispose(); - TokenCell.Entry.Completed -= Entry_Completed; - } - - MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); - MessagingCenter.Unsubscribe(Application.Current, "ResumeYubiKey"); - } - - protected override bool OnBackButtonPressed() - { - // ref: https://github.com/bitwarden/mobile/issues/350 - if(Device.RuntimePlatform == Device.Android && _providerType.HasValue && - _providerType.Value == TwoFactorProviderType.YubiKey) - { - return true; - } - return base.OnBackButtonPressed(); - } - - private async void AnotherMethodAsync() - { - var beforeProviderType = _providerType; - - var options = new List(); - if(_providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) - { - options.Add(_duoOrgTitle); - } - - if(_providers.ContainsKey(TwoFactorProviderType.Authenticator)) - { - options.Add(AppResources.AuthenticatorAppTitle); - } - - if(_providers.ContainsKey(TwoFactorProviderType.Duo)) - { - options.Add("Duo"); - } - - if(_providers.ContainsKey(TwoFactorProviderType.YubiKey)) - { - var nfcKey = _providers[TwoFactorProviderType.YubiKey].ContainsKey("Nfc") && - (bool)_providers[TwoFactorProviderType.YubiKey]["Nfc"]; - if((_deviceInfoService.NfcEnabled && nfcKey) || Device.RuntimePlatform != Device.iOS) - { - options.Add(AppResources.YubiKeyTitle); - } - } - - if(_providers.ContainsKey(TwoFactorProviderType.Email)) - { - options.Add(AppResources.Email); - } - - options.Add(AppResources.RecoveryCodeTitle); - - var selection = await DisplayActionSheet(AppResources.TwoStepLoginOptions, AppResources.Cancel, null, - options.ToArray()); - if(selection == AppResources.AuthenticatorAppTitle) - { - _providerType = TwoFactorProviderType.Authenticator; - } - else if(selection == "Duo") - { - _providerType = TwoFactorProviderType.Duo; - } - else if(selection == _duoOrgTitle) - { - _providerType = TwoFactorProviderType.OrganizationDuo; - } - else if(selection == AppResources.YubiKeyTitle) - { - _providerType = TwoFactorProviderType.YubiKey; - } - else if(selection == AppResources.Email) - { - _providerType = TwoFactorProviderType.Email; - } - else if(selection == AppResources.RecoveryCodeTitle) - { - Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/")); - return; - } - - if(beforeProviderType != _providerType) - { - Init(); - ListenYubiKey(false, beforeProviderType == TwoFactorProviderType.YubiKey); - ListenYubiKey(true); - InitEvents(); - } - } - - private async Task SendEmailAsync(bool doToast) - { - if(_providerType != TwoFactorProviderType.Email) - { - return; - } - - var response = await _twoFactorApiRepository.PostSendEmailLoginAsync(new Models.Api.TwoFactorEmailRequest - { - Email = _email, - MasterPasswordHash = _masterPasswordHash - }); - - if(response.Succeeded && doToast) - { - _deviceActionService.Toast(AppResources.VerificationEmailSent); - } - else if(!response.Succeeded) - { - await DisplayAlert(null, AppResources.VerificationEmailNotSent, AppResources.Ok); - } - } - - private async void Entry_Completed(object sender, EventArgs e) - { - var token = TokenCell.Entry.Text.Trim().Replace(" ", ""); - await LogInAsync(token); - } - - private async Task LogInAsync(string token) - { - if(!_providerType.HasValue || _lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(string.IsNullOrWhiteSpace(token)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.VerificationCode), AppResources.Ok); - return; - } - - await _deviceActionService.ShowLoadingAsync(string.Concat(AppResources.Validating, "...")); - var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On, - _email, _masterPasswordHash, _key); - await _deviceActionService.HideLoadingAsync(); - - if(!response.Success) - { - ListenYubiKey(true); - await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok); - return; - } - - _googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step", _providerType.Value.ToString()); - - var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); - Device.BeginInvokeOnMainThread(() => - { - Application.Current.MainPage = new MainPage(); - }); - } - - private TwoFactorProviderType? GetDefaultProvider() - { - TwoFactorProviderType? provider = null; - - if(_providers != null) - { - foreach(var p in _providers) - { - switch(p.Key) - { - case TwoFactorProviderType.Authenticator: - if(provider == TwoFactorProviderType.Duo || provider == TwoFactorProviderType.YubiKey || - provider == TwoFactorProviderType.OrganizationDuo) - { - continue; - } - break; - case TwoFactorProviderType.Email: - if(provider.HasValue) - { - continue; - } - break; - case TwoFactorProviderType.Duo: - if(provider == TwoFactorProviderType.YubiKey || - provider == TwoFactorProviderType.OrganizationDuo) - { - continue; - } - break; - case TwoFactorProviderType.YubiKey: - if(provider == TwoFactorProviderType.OrganizationDuo) - { - continue; - } - - var nfcKey = p.Value.ContainsKey("Nfc") && (bool)p.Value["Nfc"]; - if((!_deviceInfoService.NfcEnabled || !nfcKey) && Device.RuntimePlatform == Device.iOS) - { - continue; - } - break; - case TwoFactorProviderType.OrganizationDuo: - break; - default: - continue; - } - - provider = p.Key; - } - } - - return provider; - } - - private void ListenYubiKey(bool listen, bool overrideCheck = false) - { - if(_providerType == TwoFactorProviderType.YubiKey || overrideCheck) - { - MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen); - } - } - - private void SubscribeYubiKey(bool subscribe) - { - if(_providerType != TwoFactorProviderType.YubiKey) - { - return; - } - - MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); - MessagingCenter.Unsubscribe(Application.Current, "ResumeYubiKey"); - if(!subscribe) - { - return; - } - - MessagingCenter.Subscribe(Application.Current, "GotYubiKeyOTP", async (sender, otp) => - { - MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); - if(_providerType == TwoFactorProviderType.YubiKey) - { - await LogInAsync(otp); - } - }); - - SubscribeYubiKeyResume(); - } - - private void SubscribeYubiKeyResume() - { - MessagingCenter.Subscribe(Application.Current, "ResumeYubiKey", (sender) => - { - MessagingCenter.Unsubscribe(Application.Current, "ResumeYubiKey"); - if(_providerType == TwoFactorProviderType.YubiKey) - { - MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", true); - SubscribeYubiKeyResume(); - } - }); - } - - public class TwoFactorTable : ExtendedTableView - { - public TwoFactorTable(TableSection section) - { - Intent = TableIntent.Settings; - EnableScrolling = false; - HasUnevenRows = true; - EnableSelection = true; - NoFooter = true; - NoHeader = true; - VerticalOptions = LayoutOptions.Start; - Root = new TableRoot - { - section - }; - - if(Device.RuntimePlatform == Device.iOS) - { - RowHeight = -1; - EstimatedRowHeight = 70; - } - } - } - } -} diff --git a/src/App/Pages/MainPage.cs b/src/App/Pages/MainPage.cs deleted file mode 100644 index 545d9a0b0..000000000 --- a/src/App/Pages/MainPage.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Bit.App.Controls; -using Xamarin.Forms; - -namespace Bit.App.Pages -{ - public class MainPage : ExtendedTabbedPage - { - private ExtendedNavigationPage _vaultPage; - - public MainPage() - { - TintColor = Color.FromHex("3c8dbc"); - - _vaultPage = new ExtendedNavigationPage(new VaultListGroupingsPage()); - var passwordGeneratorNavigation = new ExtendedNavigationPage(new ToolsPasswordGeneratorPage(this)); - var toolsNavigation = new ExtendedNavigationPage(new ToolsPage(this)); - var settingsNavigation = new ExtendedNavigationPage(new SettingsPage(this)); - - _vaultPage.Icon = "fa_lock.png"; - passwordGeneratorNavigation.Icon = "refresh.png"; - toolsNavigation.Icon = "tools.png"; - settingsNavigation.Icon = "cogs.png"; - - Children.Add(_vaultPage); - Children.Add(passwordGeneratorNavigation); - Children.Add(toolsNavigation); - Children.Add(settingsNavigation); - } - - public void ResetToVaultPage() - { - CurrentPage = _vaultPage; - } - } -} diff --git a/src/App/Pages/PasswordHintPage.cs b/src/App/Pages/PasswordHintPage.cs deleted file mode 100644 index f6ca02744..000000000 --- a/src/App/Pages/PasswordHintPage.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Api; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class PasswordHintPage : ExtendedContentPage - { - private IAccountsApiRepository _accountApiRepository; - private IDeviceActionService _deviceActionService; - - public PasswordHintPage() - : base(updateActivity: false, requireAuth: false) - { - _accountApiRepository = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - Init(); - } - - public FormEntryCell EmailCell { get; set; } - - private void Init() - { - var padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(15, 8), - Windows: new Thickness(10, 8)); - - EmailCell = new FormEntryCell(AppResources.EmailAddress, entryKeyboard: Keyboard.Email, - useLabelAsPlaceholder: true, imageSource: "envelope.png", containerPadding: padding); - - EmailCell.Entry.TargetReturnType = Enums.ReturnType.Go; - - var table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = false, - HasUnevenRows = true, - EnableSelection = true, - NoFooter = true, - VerticalOptions = LayoutOptions.Start, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - EmailCell - } - } - }; - - var hintLabel = new Label - { - Text = AppResources.EnterEmailForHint, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25) - }; - - var layout = new RedrawableStackLayout - { - Children = { table, hintLabel }, - Spacing = 0 - }; - - table.WrappingStackLayout = () => layout; - var scrollView = new ScrollView { Content = layout }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 70; - } - - var submitToolbarItem = new ToolbarItem(AppResources.Submit, Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - await SubmitAsync(); - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(submitToolbarItem); - Title = AppResources.PasswordHint; - Content = scrollView; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - EmailCell.InitEvents(); - EmailCell.Entry.Completed += Entry_Completed; - EmailCell.Entry.FocusWithDelay(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - EmailCell.Dispose(); - EmailCell.Entry.Completed -= Entry_Completed; - } - - private async void Entry_Completed(object sender, EventArgs e) - { - await SubmitAsync(); - } - - private async Task SubmitAsync() - { - if(string.IsNullOrWhiteSpace(EmailCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.EmailAddress), AppResources.Ok); - return; - } - - var request = new PasswordHintRequest - { - Email = EmailCell.Entry.Text - }; - - await _deviceActionService.ShowLoadingAsync(AppResources.Submitting); - var response = await _accountApiRepository.PostPasswordHintAsync(request); - await _deviceActionService.HideLoadingAsync(); - - if(!response.Succeeded) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, response.Errors.FirstOrDefault()?.Message, AppResources.Ok); - return; - } - else - { - await DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok); - } - - await Navigation.PopAsync(); - } - } -} diff --git a/src/App/Pages/RegisterPage.cs b/src/App/Pages/RegisterPage.cs deleted file mode 100644 index 669e1a8a1..000000000 --- a/src/App/Pages/RegisterPage.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Api; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class RegisterPage : ExtendedContentPage - { - private ICryptoService _cryptoService; - private IDeviceActionService _deviceActionService; - private IAccountsApiRepository _accountsApiRepository; - private IGoogleAnalyticsService _googleAnalyticsService; - private HomePage _homePage; - - public RegisterPage(HomePage homePage) - : base(updateActivity: false, requireAuth: false) - { - _homePage = homePage; - _cryptoService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _accountsApiRepository = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell EmailCell { get; set; } - public FormEntryCell PasswordCell { get; set; } - public FormEntryCell ConfirmPasswordCell { get; set; } - public FormEntryCell PasswordHintCell { get; set; } - public RedrawableStackLayout StackLayout { get; set; } - public Label PasswordLabel { get; set; } - public Label HintLabel { get; set; } - - private void Init() - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - - var padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(15, 8), - Windows: new Thickness(10, 8)); - - PasswordHintCell = new FormEntryCell(AppResources.MasterPasswordHint, useLabelAsPlaceholder: true, - imageSource: "lightbulb.png", containerPadding: padding); - ConfirmPasswordCell = new FormEntryCell(AppResources.RetypeMasterPassword, isPassword: true, - nextElement: PasswordHintCell.Entry, useLabelAsPlaceholder: true, imageSource: "lock.png", - containerPadding: padding); - PasswordCell = new FormEntryCell(AppResources.MasterPassword, isPassword: true, - nextElement: ConfirmPasswordCell.Entry, useLabelAsPlaceholder: true, imageSource: "lock.png", - containerPadding: padding); - EmailCell = new FormEntryCell(AppResources.EmailAddress, nextElement: PasswordCell.Entry, - entryKeyboard: Keyboard.Email, useLabelAsPlaceholder: true, imageSource: "envelope.png", - containerPadding: padding); - - PasswordHintCell.Entry.TargetReturnType = Enums.ReturnType.Done; - - var table = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - EmailCell, - PasswordCell - } - } - }; - - PasswordLabel = new Label - { - Text = AppResources.MasterPasswordDescription, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25) - }; - - var table2 = new FormTableView(this) - { - NoHeader = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - ConfirmPasswordCell, - PasswordHintCell - } - } - }; - - HintLabel = new Label - { - Text = AppResources.MasterPasswordHintDescription, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25) - }; - - StackLayout = new RedrawableStackLayout - { - Children = { table, PasswordLabel, table2, HintLabel }, - Spacing = 0 - }; - - var scrollView = new ScrollView - { - Content = StackLayout - }; - - var loginToolbarItem = new ToolbarItem(AppResources.Submit, Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - await Register(); - }, ToolbarItemOrder.Default, 0); - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = table2.RowHeight = -1; - table.EstimatedRowHeight = table2.EstimatedRowHeight = 70; - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel, () => - { - MessagingCenter.Send(Application.Current, "ShowStatusBar", false); - })); - } - - ToolbarItems.Add(loginToolbarItem); - Title = AppResources.CreateAccount; - Content = scrollView; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - MessagingCenter.Send(Application.Current, "ShowStatusBar", true); - EmailCell.InitEvents(); - PasswordCell.InitEvents(); - PasswordHintCell.InitEvents(); - ConfirmPasswordCell.InitEvents(); - PasswordHintCell.Entry.Completed += Entry_Completed; - EmailCell.Entry.FocusWithDelay(); - } - protected override void OnDisappearing() - { - base.OnDisappearing(); - EmailCell.Dispose(); - PasswordCell.Dispose(); - PasswordHintCell.Dispose(); - ConfirmPasswordCell.Dispose(); - PasswordHintCell.Entry.Completed -= Entry_Completed; - } - - private async void Entry_Completed(object sender, EventArgs e) - { - await Register(); - } - - private async Task Register() - { - if(string.IsNullOrWhiteSpace(EmailCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress), AppResources.Ok); - return; - } - - if(string.IsNullOrWhiteSpace(PasswordCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok); - return; - } - - if(PasswordCell.Entry.Text.Length < 8) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.MasterPasswordLengthValMessage, - AppResources.Ok); - return; - } - - if(ConfirmPasswordCell.Entry.Text != PasswordCell.Entry.Text) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.MasterPasswordConfirmationValMessage, - AppResources.Ok); - return; - } - - var kdf = Enums.KdfType.PBKDF2_SHA256; - var kdfIterations = 100000; - var normalizedEmail = EmailCell.Entry.Text.ToLower().Trim(); - var key = _cryptoService.MakeKeyFromPassword(PasswordCell.Entry.Text, normalizedEmail, kdf, kdfIterations); - var encKey = _cryptoService.MakeEncKey(key); - var request = new RegisterRequest - { - Email = normalizedEmail, - MasterPasswordHash = _cryptoService.HashPasswordBase64(key, PasswordCell.Entry.Text), - MasterPasswordHint = !string.IsNullOrWhiteSpace(PasswordHintCell.Entry.Text) - ? PasswordHintCell.Entry.Text : null, - Key = encKey.Item2.EncryptedString, - Kdf = kdf, - KdfIterations = kdfIterations - }; - - await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount); - var response = await _accountsApiRepository.PostRegisterAsync(request); - await _deviceActionService.HideLoadingAsync(); - - if(!response.Succeeded) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, response.Errors.FirstOrDefault()?.Message, - AppResources.Ok); - return; - } - - _googleAnalyticsService.TrackAppEvent("Registered"); - await _homePage.DismissRegisterAndLoginAsync(normalizedEmail); - } - - private class FormTableView : ExtendedTableView - { - public FormTableView(RegisterPage page) - { - Intent = TableIntent.Settings; - EnableScrolling = false; - HasUnevenRows = true; - EnableSelection = true; - VerticalOptions = LayoutOptions.Start; - NoFooter = true; - WrappingStackLayout = () => page.StackLayout; - } - } - } -} diff --git a/src/App/Pages/ScanPage.cs b/src/App/Pages/ScanPage.cs deleted file mode 100644 index 4dc844f23..000000000 --- a/src/App/Pages/ScanPage.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Bit.App.Controls; -using Bit.App.Resources; -using System; -using System.Collections.Generic; -using Xamarin.Forms; -using ZXing.Net.Mobile.Forms; - -namespace Bit.App.Pages -{ - public class ScanPage : ExtendedContentPage - { - private readonly ZXingScannerView _zxing; - private readonly OverlayGrid _overlay; - private DateTime? _timerStarted = null; - private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(3); - - public ScanPage(Action callback) - : base(updateActivity: false, requireAuth: false) - { - _zxing = new ZXingScannerView - { - HorizontalOptions = LayoutOptions.FillAndExpand, - VerticalOptions = LayoutOptions.FillAndExpand, - AutomationId = "zxingScannerView", - Options = new ZXing.Mobile.MobileBarcodeScanningOptions - { - UseNativeScanning = true, - PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, - AutoRotate = false - } - }; - - _zxing.OnScanResult += (result) => - { - // Stop analysis until we navigate away so we don't keep reading barcodes - _zxing.IsAnalyzing = false; - _zxing.IsScanning = false; - - Uri uri; - if(!string.IsNullOrWhiteSpace(result.Text) && Uri.TryCreate(result.Text, UriKind.Absolute, out uri) && - !string.IsNullOrWhiteSpace(uri.Query)) - { - var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&'); - foreach(var part in queryParts) - { - if(part.StartsWith("secret=")) - { - callback(part.Substring(7)?.ToUpperInvariant()); - return; - } - } - } - - callback(null); - }; - - _overlay = new OverlayGrid - { - AutomationId = "zxingDefaultOverlay" - }; - - _overlay.TopLabel.Text = AppResources.CameraInstructionTop; - _overlay.BottomLabel.Text = AppResources.CameraInstructionBottom; - - var grid = new Grid - { - VerticalOptions = LayoutOptions.FillAndExpand, - HorizontalOptions = LayoutOptions.FillAndExpand, - Children = { _zxing, _overlay } - }; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.ScanQrTitle; - Content = grid; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - _zxing.IsScanning = true; - _timerStarted = DateTime.Now; - Device.StartTimer(new TimeSpan(0, 0, 2), () => - { - if(_timerStarted == null || (DateTime.Now - _timerStarted) > _timerMaxLength) - { - return false; - } - - _zxing.AutoFocus(); - return true; - }); - } - - protected override void OnDisappearing() - { - _timerStarted = null; - _zxing.IsScanning = false; - base.OnDisappearing(); - } - - public class OverlayGrid : Grid - { - public OverlayGrid() - { - VerticalOptions = LayoutOptions.FillAndExpand; - HorizontalOptions = LayoutOptions.FillAndExpand; - - RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); - RowDefinitions.Add(new RowDefinition { Height = new GridLength(2, GridUnitType.Star) }); - RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); - - Children.Add(new BoxView - { - VerticalOptions = LayoutOptions.Fill, - HorizontalOptions = LayoutOptions.FillAndExpand, - BackgroundColor = Color.Black, - Opacity = 0.7, - }, 0, 0); - - Children.Add(new BoxView - { - VerticalOptions = LayoutOptions.Center, - HorizontalOptions = LayoutOptions.FillAndExpand, - BackgroundColor = Color.Transparent - }, 0, 1); - - Children.Add(new BoxView - { - VerticalOptions = LayoutOptions.Fill, - HorizontalOptions = LayoutOptions.FillAndExpand, - BackgroundColor = Color.Black, - Opacity = 0.7, - }, 0, 2); - - TopLabel = new Label - { - VerticalOptions = LayoutOptions.Center, - HorizontalOptions = LayoutOptions.Center, - TextColor = Color.White, - AutomationId = "zxingDefaultOverlay_TopTextLabel", - }; - Children.Add(TopLabel, 0, 0); - - BottomLabel = new Label - { - VerticalOptions = LayoutOptions.Center, - HorizontalOptions = LayoutOptions.Center, - TextColor = Color.White, - AutomationId = "zxingDefaultOverlay_BottomTextLabel", - }; - Children.Add(BottomLabel, 0, 2); - } - - public Label TopLabel { get; set; } - public Label BottomLabel { get; set; } - } - } -} diff --git a/src/App/Pages/Settings/FolderAddEditPage.xaml b/src/App/Pages/Settings/FolderAddEditPage.xaml new file mode 100644 index 000000000..ac1dfcd4a --- /dev/null +++ b/src/App/Pages/Settings/FolderAddEditPage.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/FolderAddEditPage.xaml.cs b/src/App/Pages/Settings/FolderAddEditPage.xaml.cs new file mode 100644 index 000000000..41a6c47fc --- /dev/null +++ b/src/App/Pages/Settings/FolderAddEditPage.xaml.cs @@ -0,0 +1,51 @@ +namespace Bit.App.Pages +{ + public partial class FolderAddEditPage : BaseContentPage + { + private FolderAddEditPageViewModel _vm; + + public FolderAddEditPage( + string folderId = null) + { + InitializeComponent(); + _vm = BindingContext as FolderAddEditPageViewModel; + _vm.Page = this; + _vm.FolderId = folderId; + _vm.Init(); + SetActivityIndicator(); + if(!_vm.EditMode) + { + ToolbarItems.Remove(_deleteItem); + } + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_scrollView, true, async () => + { + await _vm.LoadAsync(); + if(!_vm.EditMode) + { + RequestFocus(_nameEntry); + } + }); + } + + private async void Save_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void Delete_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.DeleteAsync(); + } + } + } +} diff --git a/src/App/Pages/Settings/FolderAddEditPageViewModel.cs b/src/App/Pages/Settings/FolderAddEditPageViewModel.cs new file mode 100644 index 000000000..4350a307a --- /dev/null +++ b/src/App/Pages/Settings/FolderAddEditPageViewModel.cs @@ -0,0 +1,109 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class FolderAddEditPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly IFolderService _folderService; + private readonly IPlatformUtilsService _platformUtilsService; + private FolderView _folder; + + public FolderAddEditPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _folderService = ServiceContainer.Resolve("folderService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + } + + public string FolderId { get; set; } + public FolderView Folder + { + get => _folder; + set => SetProperty(ref _folder, value); + } + public bool EditMode => !string.IsNullOrWhiteSpace(FolderId); + + public void Init() + { + PageTitle = EditMode ? AppResources.EditFolder : AppResources.AddFolder; + } + + public async Task LoadAsync() + { + if(Folder == null) + { + if(EditMode) + { + var folder = await _folderService.GetAsync(FolderId); + Folder = await folder.DecryptAsync(); + } + else + { + Folder = new FolderView(); + } + } + } + + public async Task SubmitAsync() + { + if(string.IsNullOrWhiteSpace(Folder.Name)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.Name), + AppResources.Ok); + return false; + } + + var folder = await _folderService.EncryptAsync(Folder); + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _folderService.SaveWithServerAsync(folder); + Folder.Id = folder.Id; + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, + EditMode ? AppResources.FolderUpdated : AppResources.FolderCreated); + await Page.Navigation.PopModalAsync(); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + + public async Task DeleteAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete, + null, AppResources.Yes, AppResources.No); + if(!confirmed) + { + return false; + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); + await _folderService.DeleteWithServerAsync(Folder.Id); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.FolderDeleted); + await Page.Navigation.PopModalAsync(); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + } +} diff --git a/src/App/Pages/Settings/FoldersPage.xaml b/src/App/Pages/Settings/FoldersPage.xaml new file mode 100644 index 000000000..ff0369474 --- /dev/null +++ b/src/App/Pages/Settings/FoldersPage.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/FoldersPage.xaml.cs b/src/App/Pages/Settings/FoldersPage.xaml.cs new file mode 100644 index 000000000..eb3aa7f27 --- /dev/null +++ b/src/App/Pages/Settings/FoldersPage.xaml.cs @@ -0,0 +1,58 @@ +using Bit.Core.Models.View; +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class FoldersPage : BaseContentPage + { + private FoldersPageViewModel _vm; + + public FoldersPage() + { + InitializeComponent(); + SetActivityIndicator(_mainContent); + _vm = BindingContext as FoldersPageViewModel; + _vm.Page = this; + + if(Device.RuntimePlatform == Device.iOS) + { + _absLayout.Children.Remove(_fab); + } + else + { + _fab.Clicked = AddButton_Clicked; + } + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, true, async () => + { + await _vm.InitAsync(); + }, _mainContent); + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(!DoOnce()) + { + return; + } + if(!(e.SelectedItem is FolderView folder)) + { + return; + } + var page = new FolderAddEditPage(folder.Id); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + + private async void AddButton_Clicked(object sender, EventArgs e) + { + var page = new FolderAddEditPage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } +} diff --git a/src/App/Pages/Settings/FoldersPageViewModel.cs b/src/App/Pages/Settings/FoldersPageViewModel.cs new file mode 100644 index 000000000..8c2019ae6 --- /dev/null +++ b/src/App/Pages/Settings/FoldersPageViewModel.cs @@ -0,0 +1,47 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class FoldersPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IFolderService _folderService; + + private bool _showNoData; + + public FoldersPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _folderService = ServiceContainer.Resolve("folderService"); + + PageTitle = AppResources.Folders; + Folders = new ExtendedObservableCollection(); + } + + public ExtendedObservableCollection Folders { get; set; } + + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } + + public async Task InitAsync() + { + var folders = await _folderService.GetAllDecryptedAsync(); + // Remove "No Folder" + if(folders?.Any() ?? false) + { + folders.Remove(folders.Last()); + } + Folders.ResetWithRange(folders ?? new List()); + ShowNoData = Folders.Count == 0; + } + } +} diff --git a/src/App/Pages/Settings/SettingsAboutPage.cs b/src/App/Pages/Settings/SettingsAboutPage.cs deleted file mode 100644 index b574da1d7..000000000 --- a/src/App/Pages/Settings/SettingsAboutPage.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using Bit.App.Abstractions; -using XLabs.Ioc; -using Bit.App.Resources; -using FFImageLoading.Forms; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsAboutPage : ExtendedContentPage - { - private readonly IAppInfoService _appInfoService; - - public SettingsAboutPage() - { - _appInfoService = Resolver.Resolve(); - Init(); - } - - public ExtendedTextCell CreditsCell { get; set; } - - public void Init() - { - var logo = new CachedImage - { - Source = "logo.png", - HorizontalOptions = LayoutOptions.Center, - WidthRequest = 282, - HeightRequest = 44 - }; - - var versionLabel = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - Text = $@"{AppResources.Version} {_appInfoService.Version} ({_appInfoService.Build}) -© 8bit Solutions LLC 2015-{DateTime.Now.Year}", - HorizontalTextAlignment = TextAlignment.Center - }; - - var logoVersionStackLayout = new StackLayout - { - Children = { logo, versionLabel }, - Spacing = 20, - Padding = new Thickness(0, 40, 0, 0) - }; - - CreditsCell = new ExtendedTextCell - { - Text = AppResources.Credits, - ShowDisclousure = true - }; - - var table = new ExtendedTableView - { - VerticalOptions = LayoutOptions.Start, - EnableScrolling = false, - Intent = TableIntent.Settings, - HasUnevenRows = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - CreditsCell - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 44; - } - - var stackLayout = new RedrawableStackLayout - { - Children = { logoVersionStackLayout, table }, - Spacing = 0 - }; - - table.WrappingStackLayout = () => stackLayout; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.About; - Content = new ScrollView { Content = stackLayout }; - } - - private void RateCell_Tapped(object sender, EventArgs e) - { - Navigation.PushAsync(new SettingsCreditsPage()); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - CreditsCell.Tapped += RateCell_Tapped; - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - CreditsCell.Tapped -= RateCell_Tapped; - } - } -} diff --git a/src/App/Pages/Settings/SettingsAddFolderPage.cs b/src/App/Pages/Settings/SettingsAddFolderPage.cs deleted file mode 100644 index 2802cb254..000000000 --- a/src/App/Pages/Settings/SettingsAddFolderPage.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models; -using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Linq; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsAddFolderPage : ExtendedContentPage - { - private readonly IFolderService _folderService; - private readonly IDeviceActionService _deviceActionService; - private readonly IConnectivity _connectivity; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private DateTime? _lastAction; - - public SettingsAddFolderPage() - { - _folderService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell NameCell { get; set; } - - private void Init() - { - NameCell = new FormEntryCell(AppResources.Name); - - var table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = false, - HasUnevenRows = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - NameCell - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 70; - } - else if(Device.RuntimePlatform == Device.Android) - { - table.BottomPadding = 50; - } - - var saveToolBarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(string.IsNullOrWhiteSpace(NameCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), AppResources.Ok); - return; - } - - var folder = new Folder - { - Name = NameCell.Entry.Text.Encrypt() - }; - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveResult = await _folderService.SaveAsync(folder); - await _deviceActionService.HideLoadingAsync(); - - if(saveResult.Succeeded) - { - _deviceActionService.Toast(AppResources.FolderCreated); - _googleAnalyticsService.TrackAppEvent("CreatedFolder"); - await Navigation.PopForDeviceAsync(); - } - else if(saveResult.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveResult.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - - Title = AppResources.AddFolder; - Content = table; - ToolbarItems.Add(saveToolBarItem); - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - } - - protected override void OnAppearing() - { - base.OnAppearing(); - NameCell.InitEvents(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - NameCell.Dispose(); - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, AppResources.Ok); - } - } -} diff --git a/src/App/Pages/Settings/SettingsCreditsPage.cs b/src/App/Pages/Settings/SettingsCreditsPage.cs deleted file mode 100644 index 5fc2a6b01..000000000 --- a/src/App/Pages/Settings/SettingsCreditsPage.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using Bit.App.Resources; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsCreditsPage : ExtendedContentPage - { - public SettingsCreditsPage() - { - Init(); - } - - public void Init() - { - var table = new ExtendedTableView - { - EnableScrolling = true, - Intent = TableIntent.Settings, - HasUnevenRows = true, - EnableSelection = false, - Root = new TableRoot - { - new TableSection(AppResources.Translations) - { - new CustomViewCell(@"@felixqu - Chinese Simplified -@thomassth - Chinese Traditional -@Primokorn, @maxlandry - French -@bestHippos - Italian -@SW1FT - Portuguese -@majod - Slovak -@King-Tut-Tut - Swedish -@Igetin - Finnish") - }, - new TableSection(AppResources.Icons) - { - new CustomViewCell(@"Tools by Alex Auda Samora from the Noun Project -Fingerprint by masterpage.com from the Noun Project") - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 100; - } - - Title = AppResources.ThankYou; - Content = table; - } - - public class CustomViewCell : ViewCell - { - public CustomViewCell(string text) - { - var label = new Label - { - LineBreakMode = LineBreakMode.WordWrap, - Text = text, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)) - }; - - var layout = new StackLayout - { - Children = { label }, - Padding = Helpers.OnPlatform( - iOS: new Thickness(15, 20), - Android: new Thickness(16, 20), - Windows: new Thickness(10, 8)), - BackgroundColor = Color.White - }; - - if(Device.RuntimePlatform == Device.Android) - { - label.TextColor = Color.Black; - } - - View = layout; - } - } - } -} diff --git a/src/App/Pages/Settings/SettingsEditFolderPage.cs b/src/App/Pages/Settings/SettingsEditFolderPage.cs deleted file mode 100644 index 30c5e921f..000000000 --- a/src/App/Pages/Settings/SettingsEditFolderPage.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Linq; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsEditFolderPage : ExtendedContentPage - { - private readonly string _folderId; - private readonly IFolderService _folderService; - private readonly IDeviceActionService _deviceActionService; - private readonly IConnectivity _connectivity; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private DateTime? _lastAction; - - public SettingsEditFolderPage(string folderId) - { - _folderId = folderId; - _folderService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public FormEntryCell NameCell { get; set; } - public ExtendedTextCell DeleteCell { get; set; } - - private void Init() - { - var folder = _folderService.GetByIdAsync(_folderId).GetAwaiter().GetResult(); - if(folder == null) - { - // TODO: handle error. navigate back? should never happen... - return; - } - - NameCell = new FormEntryCell(AppResources.Name); - NameCell.Entry.Text = folder.Name.Decrypt(); - - DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red }; - - var mainTable = new ExtendedTableView - { - Intent = TableIntent.Settings, - HasUnevenRows = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - NameCell - }, - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - DeleteCell - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - mainTable.RowHeight = -1; - mainTable.EstimatedRowHeight = 70; - } - else if(Device.RuntimePlatform == Device.Android) - { - mainTable.BottomPadding = 50; - } - - var saveToolBarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(string.IsNullOrWhiteSpace(NameCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), AppResources.Ok); - return; - } - - folder.Name = NameCell.Entry.Text.Encrypt(); - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveResult = await _folderService.SaveAsync(folder); - await _deviceActionService.HideLoadingAsync(); - - if(saveResult.Succeeded) - { - _deviceActionService.Toast(AppResources.FolderUpdated); - _googleAnalyticsService.TrackAppEvent("EditedFolder"); - await Navigation.PopForDeviceAsync(); - } - else if(saveResult.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveResult.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - - Title = AppResources.EditFolder; - Content = mainTable; - ToolbarItems.Add(saveToolBarItem); - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - } - - protected override void OnAppearing() - { - base.OnAppearing(); - NameCell.InitEvents(); - DeleteCell.Tapped += DeleteCell_Tapped; - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - NameCell.Dispose(); - DeleteCell.Tapped -= DeleteCell_Tapped; - } - - private async void DeleteCell_Tapped(object sender, EventArgs e) - { - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - // TODO: Validate the delete operation. ex. Cannot delete a folder that has ciphers in it? - - var confirmed = await DisplayAlert(null, AppResources.DoYouReallyWantToDelete, AppResources.Yes, AppResources.No); - if(!confirmed) - { - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); - var deleteTask = await _folderService.DeleteAsync(_folderId); - await _deviceActionService.HideLoadingAsync(); - - if(deleteTask.Succeeded) - { - _deviceActionService.Toast(AppResources.FolderDeleted); - await Navigation.PopForDeviceAsync(); - } - else if(deleteTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, deleteTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - } -} diff --git a/src/App/Pages/Settings/SettingsHelpPage.cs b/src/App/Pages/Settings/SettingsHelpPage.cs deleted file mode 100644 index 8f2fc5da8..000000000 --- a/src/App/Pages/Settings/SettingsHelpPage.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using Bit.App.Abstractions; -using XLabs.Ioc; -using Bit.App.Resources; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsHelpPage : ExtendedContentPage - { - private readonly IGoogleAnalyticsService _googleAnalyticsService; - - public SettingsHelpPage() - { - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public ExtendedTextCell EmailCell { get; set; } - public ExtendedTextCell WebsiteCell { get; set; } - public ExtendedTextCell BugCell { get; set; } - public RedrawableStackLayout StackLayout { get; set; } - private CustomLabel EmailLabel { get; set; } - private CustomLabel WebsiteLabel { get; set; } - private CustomLabel BugLabel { get; set; } - - public void Init() - { - EmailCell = new ExtendedTextCell - { - Text = AppResources.EmailUs, - ShowDisclousure = true - }; - - var emailTable = new CustomTableView(this) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - EmailCell - } - } - }; - - EmailLabel = new CustomLabel(this) - { - Text = AppResources.EmailUsDescription - }; - - WebsiteCell = new ExtendedTextCell - { - Text = AppResources.VisitOurWebsite, - ShowDisclousure = true - }; - - var websiteTable = new CustomTableView(this) - { - NoHeader = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - WebsiteCell - } - } - }; - - WebsiteLabel = new CustomLabel(this) - { - Text = AppResources.VisitOurWebsiteDescription - }; - - BugCell = new ExtendedTextCell - { - Text = AppResources.FileBugReport, - ShowDisclousure = true - }; - - var bugTable = new CustomTableView(this) - { - NoHeader = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - BugCell - } - } - }; - - BugLabel = new CustomLabel(this) - { - Text = AppResources.FileBugReportDescription - }; - - StackLayout = new RedrawableStackLayout - { - Children = { emailTable, EmailLabel, websiteTable, WebsiteLabel, bugTable, BugLabel }, - Spacing = 0 - }; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.HelpAndFeedback; - Content = new ScrollView { Content = StackLayout }; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - EmailCell.Tapped += EmailCell_Tapped; - WebsiteCell.Tapped += WebsiteCell_Tapped; - BugCell.Tapped += BugCell_Tapped; - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - EmailCell.Tapped -= EmailCell_Tapped; - WebsiteCell.Tapped -= WebsiteCell_Tapped; - BugCell.Tapped -= BugCell_Tapped; - } - - private void EmailCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("HelpEmail"); - Device.OpenUri(new Uri("mailto:hello@bitwarden.com")); - } - - private void WebsiteCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("HelpWebsite"); - Device.OpenUri(new Uri("https://bitwarden.com/contact/")); - } - - private void BugCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("HelpBug"); - Device.OpenUri(new Uri("https://github.com/bitwarden/mobile")); - } - - private class CustomTableView : ExtendedTableView - { - public CustomTableView(SettingsHelpPage page) - { - Intent = TableIntent.Settings; - EnableScrolling = false; - HasUnevenRows = true; - EnableSelection = true; - VerticalOptions = LayoutOptions.Start; - NoFooter = true; - WrappingStackLayout = () => page.StackLayout; - - if(Device.RuntimePlatform == Device.iOS) - { - RowHeight = -1; - EstimatedRowHeight = 44; - } - } - } - - private class CustomLabel : Label - { - public CustomLabel(ContentPage page) - { - LineBreakMode = LineBreakMode.WordWrap; - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)); - Style = (Style)Application.Current.Resources["text-muted"]; - Margin = new Thickness(15, (page.IsLandscape() ? 5 : 0), 15, 25); - } - } - } -} diff --git a/src/App/Pages/Settings/SettingsListFoldersPage.cs b/src/App/Pages/Settings/SettingsListFoldersPage.cs deleted file mode 100644 index 3adb6f9d4..000000000 --- a/src/App/Pages/Settings/SettingsListFoldersPage.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Bit.App.Utilities; -using Xamarin.Forms; -using XLabs.Ioc; - -namespace Bit.App.Pages -{ - public class SettingsListFoldersPage : ExtendedContentPage - { - private readonly IFolderService _folderService; - - public SettingsListFoldersPage() - { - _folderService = Resolver.Resolve(); - Init(); - } - - public ExtendedObservableCollection Folders { get; private set; } - = new ExtendedObservableCollection(); - public ExtendedListView ListView { get; set; } - private AddFolderToolBarItem AddItem { get; set; } - public Fab Fab { get; set; } - - private void Init() - { - ListView = new ExtendedListView - { - ItemsSource = Folders, - ItemTemplate = new DataTemplate(() => new SettingsFolderListViewCell(this)) - }; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - var fabLayout = new FabLayout(ListView); - if(Device.RuntimePlatform == Device.Android) - { - Fab = new Fab(fabLayout, "plus.png", async (sender, args) => - { - await Navigation.PushForDeviceAsync(new SettingsAddFolderPage()); - }); - ListView.BottomPadding = 50; - } - else - { - AddItem = new AddFolderToolBarItem(this); - ToolbarItems.Add(AddItem); - } - - Title = AppResources.Folders; - Content = fabLayout; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - ListView.ItemSelected += FolderSelected; - AddItem?.InitEvents(); - LoadFoldersAsync().Wait(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - ListView.ItemSelected -= FolderSelected; - AddItem?.Dispose(); - } - - private async Task LoadFoldersAsync() - { - var folders = await _folderService.GetAllAsync(); - var pageFolders = folders.Select(f => new SettingsFolderPageModel(f)).OrderBy(f => f.Name); - Folders.ResetWithRange(pageFolders); - } - - private async void FolderSelected(object sender, SelectedItemChangedEventArgs e) - { - var folder = e.SelectedItem as SettingsFolderPageModel; - var page = new SettingsEditFolderPage(folder.Id); - await Navigation.PushForDeviceAsync(page); - } - - private class AddFolderToolBarItem : ExtendedToolbarItem - { - private readonly SettingsListFoldersPage _page; - - public AddFolderToolBarItem(SettingsListFoldersPage page) - { - _page = page; - Text = AppResources.Add; - Icon = "plus.png"; - ClickAction = () => ClickedItem(); - } - - private async void ClickedItem() - { - var page = new SettingsAddFolderPage(); - await _page.Navigation.PushForDeviceAsync(page); - } - } - - private class SettingsFolderListViewCell : ExtendedTextCell - { - public SettingsFolderListViewCell(SettingsListFoldersPage page) - { - this.SetBinding(TextProperty, nameof(SettingsFolderPageModel.Name)); - } - } - } -} diff --git a/src/App/Pages/Settings/SettingsOptionsPage.cs b/src/App/Pages/Settings/SettingsOptionsPage.cs deleted file mode 100644 index df7465986..000000000 --- a/src/App/Pages/Settings/SettingsOptionsPage.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Controls; -using Plugin.Settings.Abstractions; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsOptionsPage : ExtendedContentPage - { - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettings; - - public SettingsOptionsPage() - { - _settings = Resolver.Resolve(); - _appSettings = Resolver.Resolve(); - - Init(); - } - - private RedrawableStackLayout StackLayout { get; set; } - private ExtendedSwitchCell CopyTotpCell { get; set; } - private Label CopyTotpLabel { get; set; } - private ExtendedSwitchCell WebsiteIconsCell { get; set; } - private Label WebsiteIconsLabel { get; set; } - private ExtendedSwitchCell AutofillPersistNotificationCell { get; set; } - private Label AutofillPersistNotificationLabel { get; set; } - private ExtendedSwitchCell AutofillPasswordFieldCell { get; set; } - private Label AutofillPasswordFieldLabel { get; set; } - private ExtendedSwitchCell AutofillAlwaysCell { get; set; } - private Label AutofillAlwaysLabel { get; set; } - - private void Init() - { - WebsiteIconsCell = new ExtendedSwitchCell - { - Text = AppResources.DisableWebsiteIcons, - On = _appSettings.DisableWebsiteIcons - }; - - var websiteIconsTable = new FormTableView(this, true) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - WebsiteIconsCell - } - } - }; - - CopyTotpCell = new ExtendedSwitchCell - { - Text = AppResources.DisableAutoTotpCopy, - On = _settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false) - }; - - var totpTable = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - CopyTotpCell - } - } - }; - - CopyTotpLabel = new FormTableLabel(this) - { - Text = AppResources.DisableAutoTotpCopyDescription - }; - - WebsiteIconsLabel = new FormTableLabel(this) - { - Text = AppResources.DisableWebsiteIconsDescription - }; - - StackLayout = new RedrawableStackLayout - { - Children = - { - websiteIconsTable, WebsiteIconsLabel, - totpTable, CopyTotpLabel - }, - Spacing = 0 - }; - - if(Device.RuntimePlatform == Device.Android) - { - AutofillAlwaysCell = new ExtendedSwitchCell - { - Text = AppResources.AutofillAlways, - On = !_appSettings.AutofillPersistNotification && !_appSettings.AutofillPasswordField - }; - - var autofillAlwaysTable = new FormTableView(this, true) - { - Root = new TableRoot - { - new TableSection(AppResources.AutofillAccessibilityService) - { - AutofillAlwaysCell - } - } - }; - - AutofillAlwaysLabel = new FormTableLabel(this) - { - Text = AppResources.AutofillAlwaysDescription - }; - - AutofillPersistNotificationCell = new ExtendedSwitchCell - { - Text = AppResources.AutofillPersistNotification, - On = _appSettings.AutofillPersistNotification - }; - - var autofillPersistNotificationTable = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - AutofillPersistNotificationCell - } - } - }; - - AutofillPersistNotificationLabel = new FormTableLabel(this) - { - Text = AppResources.AutofillPersistNotificationDescription - }; - - AutofillPasswordFieldCell = new ExtendedSwitchCell - { - Text = AppResources.AutofillPasswordField, - On = _appSettings.AutofillPasswordField - }; - - var autofillPasswordFieldTable = new FormTableView(this) - { - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - AutofillPasswordFieldCell - } - } - }; - - AutofillPasswordFieldLabel = new FormTableLabel(this) - { - Text = AppResources.AutofillPasswordFieldDescription - }; - - StackLayout.Children.Add(autofillAlwaysTable); - StackLayout.Children.Add(AutofillAlwaysLabel); - StackLayout.Children.Add(autofillPasswordFieldTable); - StackLayout.Children.Add(AutofillPasswordFieldLabel); - StackLayout.Children.Add(autofillPersistNotificationTable); - StackLayout.Children.Add(AutofillPersistNotificationLabel); - } - - var scrollView = new ScrollView - { - Content = StackLayout - }; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.Options; - Content = scrollView; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - - WebsiteIconsCell.OnChanged += WebsiteIconsCell_Changed; - CopyTotpCell.OnChanged += CopyTotpCell_OnChanged; - - if(Device.RuntimePlatform == Device.Android) - { - AutofillAlwaysCell.OnChanged += AutofillAlwaysCell_OnChanged; - AutofillPasswordFieldCell.OnChanged += AutofillPasswordFieldCell_OnChanged; - AutofillPersistNotificationCell.OnChanged += AutofillPersistNotificationCell_OnChanged; - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - - WebsiteIconsCell.OnChanged -= WebsiteIconsCell_Changed; - CopyTotpCell.OnChanged -= CopyTotpCell_OnChanged; - - if(Device.RuntimePlatform == Device.Android) - { - AutofillAlwaysCell.OnChanged -= AutofillAlwaysCell_OnChanged; - AutofillPasswordFieldCell.OnChanged -= AutofillPasswordFieldCell_OnChanged; - AutofillPersistNotificationCell.OnChanged -= AutofillPersistNotificationCell_OnChanged; - } - } - - private void WebsiteIconsCell_Changed(object sender, ToggledEventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - _appSettings.DisableWebsiteIcons = cell.On; - } - - private void CopyTotpCell_OnChanged(object sender, ToggledEventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - _settings.AddOrUpdateValue(Constants.SettingDisableTotpCopy, cell.On); - } - - private void AutofillAlwaysCell_OnChanged(object sender, ToggledEventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - if(cell.On) - { - AutofillPasswordFieldCell.On = false; - AutofillPersistNotificationCell.On = false; - _appSettings.AutofillPersistNotification = false; - _appSettings.AutofillPasswordField = false; - } - } - - private void AutofillPersistNotificationCell_OnChanged(object sender, ToggledEventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - _appSettings.AutofillPersistNotification = cell.On; - if(cell.On) - { - AutofillPasswordFieldCell.On = false; - AutofillAlwaysCell.On = false; - } - } - - private void AutofillPasswordFieldCell_OnChanged(object sender, ToggledEventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - _appSettings.AutofillPasswordField = cell.On; - if(cell.On) - { - AutofillPersistNotificationCell.On = false; - AutofillAlwaysCell.On = false; - } - } - - private class FormTableView : ExtendedTableView - { - public FormTableView(SettingsOptionsPage page, bool header = false) - { - Intent = TableIntent.Settings; - EnableScrolling = false; - HasUnevenRows = true; - EnableSelection = true; - VerticalOptions = LayoutOptions.Start; - NoFooter = true; - NoHeader = !header; - WrappingStackLayout = () => page.StackLayout; - } - } - - private class FormTableLabel : Label - { - public FormTableLabel(Page page) - { - LineBreakMode = LineBreakMode.WordWrap; - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)); - Style = (Style)Application.Current.Resources["text-muted"]; - Margin = new Thickness(15, (page.IsLandscape() ? 5 : 0), 15, 25); - } - } - } -} diff --git a/src/App/Pages/Settings/SettingsPage.cs b/src/App/Pages/Settings/SettingsPage.cs deleted file mode 100644 index 6aeae8860..000000000 --- a/src/App/Pages/Settings/SettingsPage.cs +++ /dev/null @@ -1,552 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Controls; -using Plugin.Settings.Abstractions; -using Plugin.Fingerprint.Abstractions; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class SettingsPage : ExtendedContentPage - { - private readonly IAuthService _authService; - private readonly ISettings _settings; - private readonly IFingerprint _fingerprint; - private readonly IPushNotificationService _pushNotification; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly IDeviceActionService _deviceActionService; - private readonly IDeviceInfoService _deviceInfoService; - private readonly ILockService _lockService; - private readonly MainPage _mainPage; - - // TODO: Model binding context? - - public SettingsPage(MainPage mainPage) - { - _mainPage = mainPage; - _authService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _fingerprint = Resolver.Resolve(); - _pushNotification = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - _lockService = Resolver.Resolve(); - - Init(); - } - - private ExtendedSwitchCell PinCell { get; set; } - private ExtendedSwitchCell FingerprintCell { get; set; } - private ExtendedTextCell LockOptionsCell { get; set; } - private ExtendedTextCell TwoStepCell { get; set; } - private ExtendedTextCell ChangeMasterPasswordCell { get; set; } - private ExtendedTextCell ChangeEmailCell { get; set; } - private ExtendedTextCell FoldersCell { get; set; } - private ExtendedTextCell SyncCell { get; set; } - private ExtendedTextCell LockCell { get; set; } - private ExtendedTextCell LogOutCell { get; set; } - private ExtendedTextCell AboutCell { get; set; } - private ExtendedTextCell HelpCell { get; set; } - private ExtendedTextCell RateCell { get; set; } - private ExtendedTextCell OptionsCell { get; set; } - private LongDetailViewCell RateCellLong { get; set; } - private ExtendedTableView Table { get; set; } - - private async void Init() - { - PinCell = new ExtendedSwitchCell - { - Text = AppResources.UnlockWithPIN, - On = _settings.GetValueOrDefault(Constants.SettingPinUnlockOn, false) - }; - - LockOptionsCell = new ExtendedTextCell - { - Text = AppResources.LockOptions, - Detail = GetLockOptionsDetailsText(), - ShowDisclousure = true - }; - - TwoStepCell = new ExtendedTextCell - { - Text = AppResources.TwoStepLogin, - ShowDisclousure = true - }; - - LockCell = new ExtendedTextCell - { - Text = AppResources.Lock - }; - - var securitySecion = new TableSection(AppResources.Security) - { - LockOptionsCell, - PinCell, - LockCell, - TwoStepCell - }; - - if((await _fingerprint.GetAvailabilityAsync()) == FingerprintAvailability.Available) - { - var fingerprintName = Helpers.OnPlatform( - iOS: _deviceInfoService.HasFaceIdSupport ? AppResources.FaceID : AppResources.TouchID, - Android: AppResources.Fingerprint, - Windows: AppResources.WindowsHello); - FingerprintCell = new ExtendedSwitchCell - { - Text = string.Format(AppResources.UnlockWith, fingerprintName), - On = _settings.GetValueOrDefault(Constants.SettingFingerprintUnlockOn, false), - IsEnabled = true - }; - securitySecion.Insert(1, FingerprintCell); - } - - ChangeMasterPasswordCell = new ExtendedTextCell - { - Text = AppResources.ChangeMasterPassword, - ShowDisclousure = true - }; - - ChangeEmailCell = new ExtendedTextCell - { - Text = AppResources.ChangeEmail, - ShowDisclousure = true - }; - - FoldersCell = new ExtendedTextCell - { - Text = AppResources.Folders, - ShowDisclousure = true - }; - - SyncCell = new ExtendedTextCell - { - Text = AppResources.Sync, - ShowDisclousure = true - }; - - LogOutCell = new ExtendedTextCell - { - Text = AppResources.LogOut - }; - - AboutCell = new ExtendedTextCell - { - Text = AppResources.About, - ShowDisclousure = true - }; - - HelpCell = new ExtendedTextCell - { - Text = AppResources.HelpAndFeedback, - ShowDisclousure = true - }; - - OptionsCell = new ExtendedTextCell - { - Text = AppResources.Options, - ShowDisclousure = true - }; - - var otherSection = new TableSection(AppResources.Other) - { - OptionsCell, - AboutCell, - HelpCell - }; - - if(Device.RuntimePlatform == Device.iOS) - { - RateCellLong = new LongDetailViewCell(AppResources.RateTheApp, AppResources.RateTheAppDescriptionAppStore); - otherSection.Add(RateCellLong); - } - else - { - RateCell = new ExtendedTextCell - { - Text = AppResources.RateTheApp, - Detail = AppResources.RateTheAppDescription, - ShowDisclousure = true, - DetailLineBreakMode = LineBreakMode.WordWrap - }; - otherSection.Add(RateCell); - } - - Table = new CustomTable - { - Root = new TableRoot - { - securitySecion, - new TableSection(AppResources.Account) - { - ChangeMasterPasswordCell, - ChangeEmailCell, - LogOutCell - }, - new TableSection(AppResources.Manage) - { - FoldersCell, - SyncCell - }, - otherSection - } - }; - - Title = AppResources.Settings; - Content = Table; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - - PinCell.OnChanged += PinCell_Changed; - LockOptionsCell.Tapped += LockOptionsCell_Tapped; - TwoStepCell.Tapped += TwoStepCell_Tapped; - ChangeMasterPasswordCell.Tapped += ChangeMasterPasswordCell_Tapped; - - if(FingerprintCell != null) - { - FingerprintCell.OnChanged += FingerprintCell_Changed; - } - - ChangeEmailCell.Tapped += ChangeEmailCell_Tapped; - FoldersCell.Tapped += FoldersCell_Tapped; - SyncCell.Tapped += SyncCell_Tapped; - LockCell.Tapped += LockCell_Tapped; - LogOutCell.Tapped += LogOutCell_Tapped; - AboutCell.Tapped += AboutCell_Tapped; - HelpCell.Tapped += HelpCell_Tapped; - OptionsCell.Tapped += OptionsCell_Tapped; - - if(RateCellLong != null) - { - RateCellLong.Tapped += RateCell_Tapped; - } - - if(RateCell != null) - { - RateCell.Tapped += RateCell_Tapped; - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - - PinCell.OnChanged -= PinCell_Changed; - LockOptionsCell.Tapped -= LockOptionsCell_Tapped; - TwoStepCell.Tapped -= TwoStepCell_Tapped; - ChangeMasterPasswordCell.Tapped -= ChangeMasterPasswordCell_Tapped; - - if(FingerprintCell != null) - { - FingerprintCell.OnChanged -= FingerprintCell_Changed; - } - - ChangeEmailCell.Tapped -= ChangeEmailCell_Tapped; - FoldersCell.Tapped -= FoldersCell_Tapped; - SyncCell.Tapped -= SyncCell_Tapped; - LockCell.Tapped -= LockCell_Tapped; - LogOutCell.Tapped -= LogOutCell_Tapped; - AboutCell.Tapped -= AboutCell_Tapped; - HelpCell.Tapped -= HelpCell_Tapped; - OptionsCell.Tapped -= OptionsCell_Tapped; - - if(RateCellLong != null) - { - RateCellLong.Tapped -= RateCell_Tapped; - } - - if(RateCell != null) - { - RateCell.Tapped -= RateCell_Tapped; - } - } - - protected override bool OnBackButtonPressed() - { - if(Device.RuntimePlatform == Device.Android && _mainPage != null) - { - _mainPage.ResetToVaultPage(); - return true; - } - - return base.OnBackButtonPressed(); - } - - private async void TwoStepCell_Tapped(object sender, EventArgs e) - { - var confirmed = await DisplayAlert(null, AppResources.TwoStepLoginConfirmation, AppResources.Yes, - AppResources.Cancel); - if(!confirmed) - { - return; - } - - _googleAnalyticsService.TrackAppEvent("OpenedSetting", "TwoStep"); - Device.OpenUri(new Uri("https://help.bitwarden.com/article/setup-two-step-login/")); - } - - private async void LockOptionsCell_Tapped(object sender, EventArgs e) - { - var selection = await DisplayActionSheet(AppResources.LockOptions, AppResources.Cancel, null, - AppResources.LockOptionImmediately, AppResources.LockOption1Minute, AppResources.LockOption15Minutes, - AppResources.LockOption1Hour, AppResources.LockOption4Hours, AppResources.Never); - - if(selection == null || selection == AppResources.Cancel) - { - return; - } - - if(selection == AppResources.LockOptionImmediately) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, 0); - } - else if(selection == AppResources.LockOption1Minute) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, 60); - } - else if(selection == AppResources.LockOption15Minutes) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, 60 * 15); - } - else if(selection == AppResources.LockOption1Hour) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, 60 * 60); - } - else if(selection == AppResources.LockOption4Hours) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, 60 * 60 * 4); - } - else if(selection == AppResources.Never) - { - _settings.AddOrUpdateValue(Constants.SettingLockSeconds, -1); - } - else - { - return; - } - - LockOptionsCell.Detail = selection; - } - - private void SyncCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsSyncPage())); - } - - private void AboutCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsAboutPage())); - } - - private void RateCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("OpenedSetting", "RateApp"); - _deviceActionService.RateApp(); - } - - private void HelpCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsHelpPage())); - } - - private void LockCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("Locked"); - Device.BeginInvokeOnMainThread(async () => await _lockService.CheckLockAsync(true)); - } - - private async void LogOutCell_Tapped(object sender, EventArgs e) - { - var confirmed = await DisplayAlert(null, AppResources.LogoutConfirmation, AppResources.Yes, - AppResources.Cancel); - if(!confirmed) - { - return; - } - - _authService.LogOut(); - } - - private async void ChangeMasterPasswordCell_Tapped(object sender, EventArgs e) - { - var confirmed = await DisplayAlert(null, AppResources.ChangePasswordConfirmation, AppResources.Yes, - AppResources.Cancel); - if(!confirmed) - { - return; - } - - _googleAnalyticsService.TrackAppEvent("OpenedSetting", "ChangePassword"); - Device.OpenUri(new Uri("https://help.bitwarden.com/article/change-your-master-password/")); - } - - private async void ChangeEmailCell_Tapped(object sender, EventArgs e) - { - var confirmed = await DisplayAlert(null, AppResources.ChangeEmailConfirmation, AppResources.Yes, - AppResources.Cancel); - if(!confirmed) - { - return; - } - - _googleAnalyticsService.TrackAppEvent("OpenedSetting", "ChangeEmail"); - Device.OpenUri(new Uri("https://help.bitwarden.com/article/change-your-email/")); - } - - private void FingerprintCell_Changed(object sender, EventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - _settings.AddOrUpdateValue(Constants.SettingFingerprintUnlockOn, cell.On); - - if(cell.On) - { - _settings.AddOrUpdateValue(Constants.SettingPinUnlockOn, false); - PinCell.On = false; - } - } - - private void PinCell_Changed(object sender, EventArgs e) - { - var cell = sender as ExtendedSwitchCell; - if(cell == null) - { - return; - } - - if(cell.On && !_settings.GetValueOrDefault(Constants.SettingPinUnlockOn, false)) - { - cell.On = false; - var pinPage = new SettingsPinPage((page) => PinEntered(page)); - Navigation.PushModalAsync(new ExtendedNavigationPage(pinPage)); - } - else if(!cell.On) - { - _settings.AddOrUpdateValue(Constants.SettingPinUnlockOn, false); - } - } - - private void PinEntered(SettingsPinPage page) - { - page.PinControl.Entry.Unfocus(); - page.Navigation.PopModalAsync(); - - _authService.PIN = page.Model.PIN; - - _settings.AddOrUpdateValue(Constants.SettingPinUnlockOn, true); - _settings.AddOrUpdateValue(Constants.SettingFingerprintUnlockOn, false); - PinCell.On = true; - - if(FingerprintCell != null) - { - FingerprintCell.On = false; - } - } - - private void OptionsCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsOptionsPage())); - } - - private void FoldersCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsListFoldersPage())); - } - - private string GetLockOptionsDetailsText() - { - var lockSeconds = _settings.GetValueOrDefault(Constants.SettingLockSeconds, 60 * 15); - if(lockSeconds == -1) - { - return AppResources.Never; - } - else if(lockSeconds == 60) - { - return AppResources.LockOption1Minute; - } - else if(lockSeconds == 60 * 15) - { - return AppResources.LockOption15Minutes; - } - else if(lockSeconds == 60 * 60) - { - return AppResources.LockOption1Hour; - } - else if(lockSeconds == 60 * 60 * 4) - { - return AppResources.LockOption4Hours; - } - else - { - return AppResources.LockOptionImmediately; - } - } - - private class CustomTable : ExtendedTableView - { - public CustomTable() - { - Intent = TableIntent.Settings; - HasUnevenRows = true; - - if(Device.RuntimePlatform == Device.iOS) - { - RowHeight = -1; - EstimatedRowHeight = 44; - } - else if(Device.RuntimePlatform == Device.Android) - { - BottomPadding = 50; - } - } - } - - private class LongDetailViewCell : ExtendedViewCell - { - public LongDetailViewCell(string labelText, string detailText) - { - Label = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - VerticalOptions = LayoutOptions.CenterAndExpand, - LineBreakMode = LineBreakMode.TailTruncation, - Text = labelText - }; - - Detail = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - LineBreakMode = LineBreakMode.WordWrap, - VerticalOptions = LayoutOptions.End, - Style = (Style)Application.Current.Resources["text-muted"], - Text = detailText - }; - - var labelDetailStackLayout = new StackLayout - { - HorizontalOptions = LayoutOptions.StartAndExpand, - VerticalOptions = LayoutOptions.FillAndExpand, - Children = { Label, Detail }, - Padding = new Thickness(15) - }; - - ShowDisclousure = true; - View = labelDetailStackLayout; - } - - public Label Label { get; set; } - public Label Detail { get; set; } - } - } -} diff --git a/src/App/Pages/Settings/SettingsPage.xaml b/src/App/Pages/Settings/SettingsPage.xaml new file mode 100644 index 000000000..717e77787 --- /dev/null +++ b/src/App/Pages/Settings/SettingsPage.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/SettingsPage.xaml.cs b/src/App/Pages/Settings/SettingsPage.xaml.cs new file mode 100644 index 000000000..faac65c81 --- /dev/null +++ b/src/App/Pages/Settings/SettingsPage.xaml.cs @@ -0,0 +1,112 @@ +using Bit.App.Resources; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace Bit.App.Pages +{ + public partial class SettingsPage : BaseContentPage + { + private SettingsPageViewModel _vm; + + public SettingsPage() + { + InitializeComponent(); + _vm = BindingContext as SettingsPageViewModel; + _vm.Page = this; + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + await _vm.InitAsync(); + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(!DoOnce()) + { + return; + } + if(!(e.SelectedItem is SettingsPageListItem item)) + { + return; + } + + if(item.Name == AppResources.Sync) + { + await Navigation.PushModalAsync(new NavigationPage(new SyncPage())); + } + else if(item.Name == AppResources.Folders) + { + await Navigation.PushModalAsync(new NavigationPage(new FoldersPage())); + } + else if(item.Name == AppResources.About) + { + await _vm.AboutAsync(); + } + else if(item.Name == AppResources.HelpAndFeedback) + { + _vm.Help(); + } + else if(item.Name == AppResources.FingerprintPhrase) + { + await _vm.FingerprintAsync(); + } + else if(item.Name == AppResources.RateTheApp) + { + _vm.Rate(); + } + else if(item.Name == AppResources.ImportItems) + { + _vm.Import(); + } + else if(item.Name == AppResources.ExportVault) + { + _vm.Export(); + } + else if(item.Name == AppResources.ShareVault) + { + await _vm.ShareAsync(); + } + else if(item.Name == AppResources.WebVault) + { + _vm.WebVault(); + } + else if(item.Name == AppResources.ChangeMasterPassword) + { + await _vm.ChangePasswordAsync(); + } + else if(item.Name == AppResources.TwoStepLogin) + { + await _vm.TwoStepAsync(); + } + else if(item.Name == AppResources.LogOut) + { + await _vm.LogOutAsync(); + } + else if(item.Name == AppResources.LockNow) + { + await _vm.LockAsync(); + } + else if(item.Name == AppResources.LockOptions) + { + await _vm.LockOptionsAsync(); + } + else if(item.Name == AppResources.UnlockWithPIN) + { + await _vm.UpdatePinAsync(); + } + else if(item.Name.Contains(AppResources.Fingerprint) || item.Name.Contains(AppResources.TouchID) || + item.Name.Contains(AppResources.FaceID)) + { + await _vm.UpdateFingerprintAsync(); + } + } + } +} diff --git a/src/App/Pages/Settings/SettingsPageListGroup.cs b/src/App/Pages/Settings/SettingsPageListGroup.cs new file mode 100644 index 000000000..5b85c7771 --- /dev/null +++ b/src/App/Pages/Settings/SettingsPageListGroup.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Bit.App.Pages +{ + public class SettingsPageListGroup : List + { + public SettingsPageListGroup(List groupItems, string name, bool doUpper = true) + { + AddRange(groupItems); + if(string.IsNullOrWhiteSpace(name)) + { + Name = "-"; + } + else if(doUpper) + { + Name = name.ToUpperInvariant(); + } + else + { + Name = name; + } + } + + public string Name { get; set; } + } +} diff --git a/src/App/Pages/Settings/SettingsPageListItem.cs b/src/App/Pages/Settings/SettingsPageListItem.cs new file mode 100644 index 000000000..dce875912 --- /dev/null +++ b/src/App/Pages/Settings/SettingsPageListItem.cs @@ -0,0 +1,9 @@ +namespace Bit.App.Pages +{ + public class SettingsPageListItem + { + public string Icon { get; set; } + public string Name { get; set; } + public string SubLabel { get; set; } + } +} diff --git a/src/App/Pages/Settings/SettingsPageListItemSelector.cs b/src/App/Pages/Settings/SettingsPageListItemSelector.cs new file mode 100644 index 000000000..c58e7723f --- /dev/null +++ b/src/App/Pages/Settings/SettingsPageListItemSelector.cs @@ -0,0 +1,18 @@ +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class SettingsPageListItemSelector : DataTemplateSelector + { + public DataTemplate RegularTemplate { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + if(item is SettingsPageListItem listItem) + { + return RegularTemplate; + } + return null; + } + } +} diff --git a/src/App/Pages/Settings/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPageViewModel.cs new file mode 100644 index 000000000..2165ee938 --- /dev/null +++ b/src/App/Pages/Settings/SettingsPageViewModel.cs @@ -0,0 +1,313 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class SettingsPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IDeviceActionService _deviceActionService; + private readonly IEnvironmentService _environmentService; + private readonly IMessagingService _messagingService; + private readonly ILockService _lockService; + private readonly IStorageService _storageService; + private readonly ISyncService _syncService; + + private string _fingerprintName; + private bool _supportsFingerprint; + private bool _pin; + private bool _fingerprint; + private string _lastSyncDate; + private string _lockOptionValue; + private List> _lockOptions = + new List> + { + new KeyValuePair(AppResources.LockOptionImmediately, 0), + new KeyValuePair(AppResources.LockOption1Minute, 1), + new KeyValuePair(AppResources.LockOption5Minutes, 5), + new KeyValuePair(AppResources.LockOption15Minutes, 15), + new KeyValuePair(AppResources.LockOption30Minutes, 30), + new KeyValuePair(AppResources.LockOption1Hour, 60), + new KeyValuePair(AppResources.LockOption4Hours, 240), + new KeyValuePair(AppResources.Never, null), + }; + + public SettingsPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _userService = ServiceContainer.Resolve("userService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _environmentService = ServiceContainer.Resolve("environmentService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _lockService = ServiceContainer.Resolve("lockService"); + _storageService = ServiceContainer.Resolve("storageService"); + _syncService = ServiceContainer.Resolve("syncService"); + + GroupedItems = new ExtendedObservableCollection(); + PageTitle = AppResources.Settings; + + _fingerprintName = AppResources.Fingerprint; + if(Device.RuntimePlatform == Device.iOS) + { + _fingerprintName = AppResources.TouchID; + // TODO: face id + } + } + + public ExtendedObservableCollection GroupedItems { get; set; } + + public async Task InitAsync() + { + _supportsFingerprint = await _platformUtilsService.SupportsFingerprintAsync(); + var lastSync = await _syncService.GetLastSyncAsync(); + if(lastSync != null) + { + _lastSyncDate = string.Format("{0} {1}", lastSync.Value.ToShortDateString(), + lastSync.Value.ToShortTimeString()); + } + var option = await _storageService.GetAsync(Constants.LockOptionKey); + _lockOptionValue = _lockOptions.FirstOrDefault(o => o.Value == option).Key; + var pinSet = await _lockService.IsPinLockSetAsync(); + _pin = pinSet.Item1 || pinSet.Item2; + _fingerprint = await _lockService.IsFingerprintLockSetAsync(); + BuildList(); + } + + public async Task AboutAsync() + { + var debugText = string.Format("{0}: {1}", AppResources.Version, + _platformUtilsService.GetApplicationVersion()); + var text = string.Format("© 8bit Solutions LLC 2015-{0}\n\n{1}", DateTime.Now.Year, debugText); + var copy = await _platformUtilsService.ShowDialogAsync(text, AppResources.Bitwarden, AppResources.Copy, + AppResources.Close); + if(copy) + { + await _platformUtilsService.CopyToClipboardAsync(debugText); + } + } + + public void Help() + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/"); + } + + public async Task FingerprintAsync() + { + var fingerprint = await _cryptoService.GetFingerprintAsync(await _userService.GetUserIdAsync()); + var phrase = string.Join("-", fingerprint); + var text = string.Format("{0}:\n\n{1}", AppResources.YourAccountsFingerprint, phrase); + var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase, + AppResources.LearnMore, AppResources.Close); + if(learnMore) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/fingerprint-phrase/"); + } + } + + public void Rate() + { + _deviceActionService.RateApp(); + } + + public void Import() + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/import-data/"); + } + + public void Export() + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/export-your-data/"); + } + + public void WebVault() + { + var url = _environmentService.GetWebVaultUrl(); + if(url == null) + { + url = "https://vault.bitwarden.com"; + } + _platformUtilsService.LaunchUri(url); + } + + public async Task ShareAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ShareVaultConfirmation, + AppResources.ShareVault, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/what-is-an-organization/"); + } + } + + public async Task TwoStepAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.TwoStepLoginConfirmation, + AppResources.TwoStepLogin, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/setup-two-step-login/"); + } + } + + public async Task ChangePasswordAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ChangePasswordConfirmation, + AppResources.ChangeMasterPassword, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/change-your-master-password/"); + } + } + + public async Task LogOutAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, + AppResources.LogOut, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _messagingService.Send("logout"); + } + } + + public async Task LockAsync() + { + await _lockService.LockAsync(true); + } + + public async Task LockOptionsAsync() + { + var options = _lockOptions.Select(o => o.Key == _lockOptionValue ? $"✓ {o.Key}" : o.Key).ToArray(); + var selection = await Page.DisplayActionSheet(AppResources.LockOptions, AppResources.Cancel, null, options); + if(selection == AppResources.Cancel) + { + return; + } + var cleanSelection = selection.Replace("✓ ", string.Empty); + var selectionOption = _lockOptions.FirstOrDefault(o => o.Key == cleanSelection); + _lockOptionValue = selectionOption.Key; + await _lockService.SetLockOptionAsync(selectionOption.Value); + BuildList(); + } + + public async Task UpdatePinAsync() + { + _pin = !_pin; + if(_pin) + { + var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN, + AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true); + var masterPassOnRestart = true; + if(!string.IsNullOrWhiteSpace(pin)) + { + if(masterPassOnRestart) + { + var encPin = await _cryptoService.EncryptAsync(pin); + await _storageService.SaveAsync(Constants.ProtectedPin, encPin.EncryptedString); + } + else + { + var kdf = await _userService.GetKdfAsync(); + var kdfIterations = await _userService.GetKdfIterationsAsync(); + var email = await _userService.GetEmailAsync(); + var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email, + kdf.GetValueOrDefault(Core.Enums.KdfType.PBKDF2_SHA256), + kdfIterations.GetValueOrDefault(5000)); + var key = await _cryptoService.GetKeyAsync(); + var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey); + await _storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString); + } + } + else + { + _pin = false; + } + } + if(!_pin) + { + await _storageService.RemoveAsync(Constants.PinProtectedKey); + await _storageService.RemoveAsync(Constants.ProtectedPin); + } + BuildList(); + } + + public async Task UpdateFingerprintAsync() + { + _fingerprint = !_fingerprint; + if(_fingerprint) + { + await _storageService.SaveAsync(Constants.FingerprintUnlockKey, true); + } + else + { + await _storageService.RemoveAsync(Constants.FingerprintUnlockKey); + } + await _cryptoService.ToggleKeyAsync(); + BuildList(); + } + + private void BuildList() + { + var doUpper = Device.RuntimePlatform != Device.Android; + var manageItems = new List + { + new SettingsPageListItem { Name = AppResources.Folders }, + new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate } + }; + var securityItems = new List + { + new SettingsPageListItem { Name = AppResources.LockOptions, SubLabel = _lockOptionValue }, + new SettingsPageListItem { Name = AppResources.UnlockWithPIN, SubLabel = _pin ? "✓" : null }, + new SettingsPageListItem { Name = AppResources.LockNow }, + new SettingsPageListItem { Name = AppResources.TwoStepLogin } + }; + if(_supportsFingerprint) + { + var item = new SettingsPageListItem + { + Name = string.Format(AppResources.UnlockWith, _fingerprintName), + SubLabel = _fingerprint ? "✓" : null + }; + securityItems.Insert(1, item); + } + var accountItems = new List + { + new SettingsPageListItem { Name = AppResources.ChangeMasterPassword }, + new SettingsPageListItem { Name = AppResources.FingerprintPhrase }, + new SettingsPageListItem { Name = AppResources.LogOut } + }; + var toolsItems = new List + { + new SettingsPageListItem { Name = AppResources.ImportItems }, + new SettingsPageListItem { Name = AppResources.ExportVault }, + new SettingsPageListItem { Name = AppResources.ShareVault }, + new SettingsPageListItem { Name = AppResources.WebVault } + }; + var otherItems = new List + { + new SettingsPageListItem { Name = AppResources.Options }, + new SettingsPageListItem { Name = AppResources.About }, + new SettingsPageListItem { Name = AppResources.HelpAndFeedback }, + new SettingsPageListItem { Name = AppResources.RateTheApp } + }; + GroupedItems.ResetWithRange(new List + { + new SettingsPageListGroup(manageItems, AppResources.Manage, doUpper), + new SettingsPageListGroup(securityItems, AppResources.Security, doUpper), + new SettingsPageListGroup(accountItems, AppResources.Account, doUpper), + new SettingsPageListGroup(toolsItems, AppResources.Tools, doUpper), + new SettingsPageListGroup(otherItems, AppResources.Other, doUpper) + }); + } + } +} diff --git a/src/App/Pages/Settings/SettingsPinPage.cs b/src/App/Pages/Settings/SettingsPinPage.cs deleted file mode 100644 index e483a7da2..000000000 --- a/src/App/Pages/Settings/SettingsPinPage.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Plugin.Settings.Abstractions; -using Bit.App.Models.Page; -using Bit.App.Controls; - -namespace Bit.App.Pages -{ - public class SettingsPinPage : ExtendedContentPage - { - private readonly ISettings _settings; - private Action _pinEnteredAction; - - public SettingsPinPage(Action pinEnteredAction) - { - _pinEnteredAction = pinEnteredAction; - _settings = Resolver.Resolve(); - - Init(); - } - - public PinPageModel Model { get; set; } = new PinPageModel(); - public PinControl PinControl { get; set; } - public TapGestureRecognizer Tgr { get; set; } - - public void Init() - { - var instructionLabel = new Label - { - Text = AppResources.SetPINDirection, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - HorizontalTextAlignment = TextAlignment.Center, - Style = (Style)Application.Current.Resources["text-muted"] - }; - - PinControl = new PinControl(); - PinControl.Label.SetBinding(Label.TextProperty, nameof(PinPageModel.LabelText)); - PinControl.Entry.SetBinding(Entry.TextProperty, nameof(PinPageModel.PIN)); - - var stackLayout = new StackLayout - { - Padding = new Thickness(30, 40), - Spacing = 20, - Children = { PinControl.Label, instructionLabel, PinControl.Entry } - }; - - Tgr = new TapGestureRecognizer(); - PinControl.Label.GestureRecognizers.Add(Tgr); - instructionLabel.GestureRecognizers.Add(Tgr); - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - - Title = AppResources.SetPIN; - Content = stackLayout; - Content.GestureRecognizers.Add(Tgr); - BindingContext = Model; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - Tgr.Tapped += Tgr_Tapped; - PinControl.OnPinEntered += PinEntered; - PinControl.InitEvents(); - PinControl.Entry.FocusWithDelay(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - PinControl.Dispose(); - Tgr.Tapped -= Tgr_Tapped; - PinControl.OnPinEntered -= PinEntered; - } - - protected void PinEntered(object sender, EventArgs args) - { - _pinEnteredAction?.Invoke(this); - } - - private void Tgr_Tapped(object sender, EventArgs e) - { - PinControl.Entry.Focus(); - } - } -} diff --git a/src/App/Pages/Settings/SettingsSyncPage.cs b/src/App/Pages/Settings/SettingsSyncPage.cs deleted file mode 100644 index d3d6ed5ce..000000000 --- a/src/App/Pages/Settings/SettingsSyncPage.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using Plugin.Settings.Abstractions; - -namespace Bit.App.Pages -{ - public class SettingsSyncPage : ExtendedContentPage - { - private readonly ISyncService _syncService; - private readonly IDeviceActionService _deviceActionService; - private readonly IConnectivity _connectivity; - private readonly ISettings _settings; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - - public SettingsSyncPage() - { - _syncService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public Label LastSyncLabel { get; set; } - - public void Init() - { - var syncButton = new ExtendedButton - { - Text = AppResources.SyncVaultNow, - Command = new Command(async () => await SyncAsync()), - Style = (Style)Application.Current.Resources["btn-primaryAccent"] - }; - - LastSyncLabel = new Label - { - Style = (Style)Application.Current.Resources["text-muted"], - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - HorizontalTextAlignment = TextAlignment.Center - }; - - SetLastSync(); - - var stackLayout = new StackLayout - { - VerticalOptions = LayoutOptions.CenterAndExpand, - Children = { syncButton, LastSyncLabel }, - Padding = new Thickness(15, 0) - }; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.Sync; - Content = stackLayout; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - } - - private void SetLastSync() - { - DateTime? lastSyncDate = null; - if(_settings.Contains(Constants.LastSync)) - { - lastSyncDate = _settings.GetValueOrDefault(Constants.LastSync, DateTime.UtcNow); - } - try - { - LastSyncLabel.Text = AppResources.LastSync + " " + lastSyncDate?.ToLocalTime().ToString() ?? AppResources.Never; - } - catch - { - // some users with different calendars have issues with ToString()ing a date - // it seems the linker is at fault. just catch for now since this isn't that important. - // ref http://bit.ly/2c2JU7b - } - } - - public async Task SyncAsync() - { - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Syncing); - var succeeded = await _syncService.FullSyncAsync(true); - await _deviceActionService.HideLoadingAsync(); - if(succeeded) - { - _deviceActionService.Toast(AppResources.SyncingComplete); - _googleAnalyticsService.TrackAppEvent("Synced"); - } - else - { - _deviceActionService.Toast(AppResources.SyncingFailed); - } - - SetLastSync(); - } - - public void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - } -} diff --git a/src/App/Pages/Settings/SyncPage.xaml b/src/App/Pages/Settings/SyncPage.xaml new file mode 100644 index 000000000..e5a7242a3 --- /dev/null +++ b/src/App/Pages/Settings/SyncPage.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/SyncPage.xaml.cs b/src/App/Pages/Settings/SyncPage.xaml.cs new file mode 100644 index 000000000..f9d1ce01a --- /dev/null +++ b/src/App/Pages/Settings/SyncPage.xaml.cs @@ -0,0 +1,30 @@ +using System; + +namespace Bit.App.Pages +{ + public partial class SyncPage : BaseContentPage + { + private readonly SyncPageViewModel _vm; + + public SyncPage() + { + InitializeComponent(); + _vm = BindingContext as SyncPageViewModel; + _vm.Page = this; + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + await _vm.SetLastSyncAsync(); + } + + private async void Sync_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SyncAsync(); + } + } + } +} diff --git a/src/App/Pages/Settings/SyncPageViewModel.cs b/src/App/Pages/Settings/SyncPageViewModel.cs new file mode 100644 index 000000000..87ae4bae9 --- /dev/null +++ b/src/App/Pages/Settings/SyncPageViewModel.cs @@ -0,0 +1,71 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class SyncPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly ISyncService _syncService; + + private string _lastSync = "--"; + + public SyncPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _syncService = ServiceContainer.Resolve("syncService"); + + PageTitle = AppResources.Sync; + } + + public string LastSync + { + get => _lastSync; + set => SetProperty(ref _lastSync, value); + } + + public async Task SetLastSyncAsync() + { + var last = await _syncService.GetLastSyncAsync(); + if(last != null) + { + var localDate = last.Value.ToLocalTime(); + LastSync = string.Format("{0} {1}", localDate.ToShortDateString(), localDate.ToShortTimeString()); + } + else + { + LastSync = AppResources.Never; + } + } + + public async Task SyncAsync() + { + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Syncing); + var success = await _syncService.FullSyncAsync(true); + await _deviceActionService.HideLoadingAsync(); + if(success) + { + await SetLastSyncAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete); + } + else + { + await Page.DisplayAlert(null, AppResources.SyncingFailed, AppResources.Ok); + } + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + } +} diff --git a/src/App/Pages/TabsPage.cs b/src/App/Pages/TabsPage.cs new file mode 100644 index 000000000..781fea707 --- /dev/null +++ b/src/App/Pages/TabsPage.cs @@ -0,0 +1,56 @@ +using Bit.App.Resources; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class TabsPage : TabbedPage + { + public TabsPage() + { + var groupingsPage = new NavigationPage(new GroupingsPage(true)) + { + Title = AppResources.MyVault, + Icon = "lock.png" + }; + Children.Add(groupingsPage); + + var generatorPage = new NavigationPage(new GeneratorPage(true, null)) + { + Title = AppResources.Generator, + Icon = "refresh.png" + }; + Children.Add(generatorPage); + + var settingsPage = new NavigationPage(new SettingsPage()) + { + Title = AppResources.Settings, + Icon = "cogs.png" + }; + Children.Add(settingsPage); + + Xamarin.Forms.PlatformConfiguration.AndroidSpecific.TabbedPage.SetToolbarPlacement(this, + Xamarin.Forms.PlatformConfiguration.AndroidSpecific.ToolbarPlacement.Bottom); + Xamarin.Forms.PlatformConfiguration.AndroidSpecific.TabbedPage.SetIsSwipePagingEnabled(this, false); + Xamarin.Forms.PlatformConfiguration.AndroidSpecific.TabbedPage.SetIsSmoothScrollEnabled(this, false); + } + + protected async override void OnCurrentPageChanged() + { + if(CurrentPage is NavigationPage navPage) + { + if(navPage.RootPage is GroupingsPage groupingsPage) + { + // Load something? + } + else if(navPage.RootPage is GeneratorPage genPage) + { + await genPage.InitAsync(); + } + else if(navPage.RootPage is SettingsPage settingsPage) + { + // Load something? + } + } + } + } +} diff --git a/src/App/Pages/Tools/ToolsAccessibilityServicePage.cs b/src/App/Pages/Tools/ToolsAccessibilityServicePage.cs deleted file mode 100644 index 16c295aef..000000000 --- a/src/App/Pages/Tools/ToolsAccessibilityServicePage.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Abstractions; -using Bit.App.Resources; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class ToolsAccessibilityServicePage : ExtendedContentPage - { - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly IAppInfoService _appInfoService; - private readonly IDeviceActionService _deviceActionService; - private DateTime? _timerStarted = null; - private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(5); - - public ToolsAccessibilityServicePage() - { - _googleAnalyticsService = Resolver.Resolve(); - _appInfoService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - - Init(); - } - - public StackLayout EnabledStackLayout { get; set; } - public StackLayout DisabledStackLayout { get; set; } - public ScrollView ScrollView { get; set; } - - public void Init() - { - var enabledFs = new FormattedString(); - var statusSpan = new Span { Text = string.Concat(AppResources.Status, " ") }; - enabledFs.Spans.Add(statusSpan); - enabledFs.Spans.Add(new Span - { - Text = AppResources.Enabled, - ForegroundColor = Color.Green, - FontAttributes = FontAttributes.Bold, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) - }); - - var statusEnabledLabel = new Label - { - FormattedText = enabledFs, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - TextColor = Color.Black - }; - - var disabledFs = new FormattedString(); - disabledFs.Spans.Add(statusSpan); - disabledFs.Spans.Add(new Span - { - Text = AppResources.Disabled, - ForegroundColor = Color.FromHex("c62929"), - FontAttributes = FontAttributes.Bold, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) - }); - - var statusDisabledLabel = new Label - { - FormattedText = disabledFs, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - TextColor = Color.Black - }; - - var step1Label = new Label - { - Text = AppResources.BitwardenAutofillServiceStep1, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - TextColor = Color.Black - }; - - var step1Image = new CachedImage - { - Source = "accessibility_step1", - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, 20, 0, 0), - WidthRequest = 300, - HeightRequest = 98 - }; - - var step2Label = new Label - { - Text = AppResources.BitwardenAutofillServiceStep2, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - TextColor = Color.Black - }; - - var step2Image = new CachedImage - { - Source = "accessibility_step2", - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, 20, 0, 0), - WidthRequest = 300, - HeightRequest = 67 - }; - - var stepsStackLayout = new StackLayout - { - Children = { statusDisabledLabel, step1Image, step1Label, step2Image, step2Label }, - Orientation = StackOrientation.Vertical, - Spacing = 10, - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center - }; - - var notificationsLabel = new Label - { - Text = AppResources.BitwardenAutofillServiceNotification, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - TextColor = Color.Black - }; - - var tapNotificationImage = new CachedImage - { - Source = "accessibility_notification.png", - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, 20, 0, 0), - WidthRequest = 300, - HeightRequest = 74 - }; - - var tapNotificationIcon = new CachedImage - { - Source = "accessibility_notification_icon.png", - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, 20, 0, 0), - WidthRequest = 300, - HeightRequest = 54 - }; - - var notificationsStackLayout = new StackLayout - { - Children = { statusEnabledLabel, tapNotificationIcon, tapNotificationImage, notificationsLabel }, - Orientation = StackOrientation.Vertical, - Spacing = 10, - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center - }; - - DisabledStackLayout = new StackLayout - { - Children = { BuildServiceLabel(), stepsStackLayout, BuildGoButton() }, - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 30), - VerticalOptions = LayoutOptions.FillAndExpand - }; - - EnabledStackLayout = new StackLayout - { - Children = { BuildServiceLabel(), notificationsStackLayout, BuildGoButton() }, - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 30), - VerticalOptions = LayoutOptions.FillAndExpand - }; - - ScrollView = new ScrollView { Content = DisabledStackLayout }; - Title = AppResources.AutofillAccessibilityService; - Content = ScrollView; - } - - protected override void OnAppearing() - { - UpdateEnabled(); - _timerStarted = DateTime.Now; - Device.StartTimer(new TimeSpan(0, 0, 3), () => - { - System.Diagnostics.Debug.WriteLine("Check timer on accessibility"); - if(_timerStarted == null || (DateTime.Now - _timerStarted) > _timerMaxLength) - { - return false; - } - - UpdateEnabled(); - return true; - }); - - base.OnAppearing(); - } - - protected override void OnDisappearing() - { - _timerStarted = null; - base.OnDisappearing(); - } - - private void UpdateEnabled() - { - ScrollView.Content = _appInfoService.AutofillAccessibilityServiceEnabled ? - EnabledStackLayout : DisabledStackLayout; - } - - private Label BuildServiceLabel() - { - return new Label - { - Text = AppResources.AutofillAccessibilityDescription, - VerticalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)) - }; - } - - private ExtendedButton BuildGoButton() - { - return new ExtendedButton - { - Text = AppResources.BitwardenAutofillServiceOpenAccessibilitySettings, - Command = new Command(() => - { - _googleAnalyticsService.TrackAppEvent("OpenAccessibilitySettings"); - _deviceActionService.OpenAccessibilitySettings(); - }), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - } - } -} diff --git a/src/App/Pages/Tools/ToolsAutofillPage.cs b/src/App/Pages/Tools/ToolsAutofillPage.cs deleted file mode 100644 index 22edc0a34..000000000 --- a/src/App/Pages/Tools/ToolsAutofillPage.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using Bit.App.Resources; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class ToolsAutofillPage : ExtendedContentPage - { - public ToolsAutofillPage() - { - Init(); - } - - public void Init() - { - var getAccessLabel = new Label - { - Text = AppResources.ExtensionInstantAccess, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), - Margin = new Thickness(0, 0, 0, 15), - }; - - var turnOnLabel = new Label - { - Text = AppResources.AutofillTurnOn, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - Margin = new Thickness(0, 0, 0, 15), - }; - - var turnOnLabel1 = new Label - { - Text = AppResources.AutofillTurnOn1, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Start, - LineBreakMode = LineBreakMode.WordWrap - }; - - var turnOnLabel2 = new Label - { - Text = AppResources.AutofillTurnOn2, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Start, - LineBreakMode = LineBreakMode.WordWrap - }; - - var turnOnLabel3 = new Label - { - Text = AppResources.AutofillTurnOn3, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Start, - LineBreakMode = LineBreakMode.WordWrap - }; - - var turnOnLabel4 = new Label - { - Text = AppResources.AutofillTurnOn4, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Start, - LineBreakMode = LineBreakMode.WordWrap - }; - - var turnOnLabel5 = new Label - { - Text = AppResources.AutofillTurnOn5, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Start, - LineBreakMode = LineBreakMode.WordWrap - }; - - var keyboardImge = new CachedImage - { - Source = "autofill-kb.png", - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, 10, 0, 0), - WidthRequest = 290, - HeightRequest = 252 - }; - - var stackLayout = new StackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 5, - Padding = new Thickness(20, 20, 20, 30), - Children = { getAccessLabel, turnOnLabel, turnOnLabel1, turnOnLabel2, - turnOnLabel3, turnOnLabel4, turnOnLabel5, keyboardImge }, - VerticalOptions = LayoutOptions.FillAndExpand - }; - - if(Device.RuntimePlatform == Device.iOS) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.PasswordAutofill; - Content = new ScrollView { Content = stackLayout }; - } - } -} diff --git a/src/App/Pages/Tools/ToolsAutofillServicePage.cs b/src/App/Pages/Tools/ToolsAutofillServicePage.cs deleted file mode 100644 index e9008856a..000000000 --- a/src/App/Pages/Tools/ToolsAutofillServicePage.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Abstractions; -using Bit.App.Resources; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class ToolsAutofillServicePage : ExtendedContentPage - { - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly IAppInfoService _appInfoService; - private readonly IDeviceActionService _deviceActionService; - private DateTime? _timerStarted = null; - private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(5); - - public ToolsAutofillServicePage() - { - _googleAnalyticsService = Resolver.Resolve(); - _appInfoService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - - Init(); - } - - public StackLayout EnabledStackLayout { get; set; } - public StackLayout DisabledStackLayout { get; set; } - public ScrollView ScrollView { get; set; } - - public void Init() - { - var enabledFs = new FormattedString(); - var statusSpan = new Span { Text = string.Concat(AppResources.Status, " ") }; - enabledFs.Spans.Add(statusSpan); - enabledFs.Spans.Add(new Span - { - Text = AppResources.Enabled, - ForegroundColor = Color.Green, - FontAttributes = FontAttributes.Bold, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) - }); - - var statusEnabledLabel = new Label - { - FormattedText = enabledFs, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - TextColor = Color.Black, - VerticalOptions = LayoutOptions.CenterAndExpand - }; - - var disabledFs = new FormattedString(); - disabledFs.Spans.Add(statusSpan); - disabledFs.Spans.Add(new Span - { - Text = AppResources.Disabled, - ForegroundColor = Color.FromHex("c62929"), - FontAttributes = FontAttributes.Bold, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) - }); - - var statusDisabledLabel = new Label - { - FormattedText = disabledFs, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - TextColor = Color.Black - }; - - var enableImage = new CachedImage - { - Source = "autofill_enable.png", - HorizontalOptions = LayoutOptions.Center, - VerticalOptions = LayoutOptions.CenterAndExpand, - WidthRequest = 300, - HeightRequest = 118 - }; - - var useImage = new CachedImage - { - Source = "autofill_use.png", - HorizontalOptions = LayoutOptions.Center, - VerticalOptions = LayoutOptions.CenterAndExpand, - WidthRequest = 300, - HeightRequest = 128 - }; - - var goButton = new ExtendedButton - { - Text = AppResources.BitwardenAutofillServiceOpenAutofillSettings, - Command = new Command(() => - { - _googleAnalyticsService.TrackAppEvent("OpenAutofillSettings"); - _deviceActionService.OpenAutofillSettings(); - }), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - - DisabledStackLayout = new StackLayout - { - Children = { BuildServiceLabel(), statusDisabledLabel, enableImage, goButton }, - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 30), - VerticalOptions = LayoutOptions.FillAndExpand - }; - - EnabledStackLayout = new StackLayout - { - Children = { BuildServiceLabel(), statusEnabledLabel, useImage }, - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 30), - VerticalOptions = LayoutOptions.FillAndExpand - }; - - ScrollView = new ScrollView { Content = DisabledStackLayout }; - Title = AppResources.AutofillService; - Content = ScrollView; - } - - protected override void OnAppearing() - { - UpdateEnabled(); - _timerStarted = DateTime.Now; - Device.StartTimer(new TimeSpan(0, 0, 2), () => - { - System.Diagnostics.Debug.WriteLine("Check timer on autofill"); - if(_timerStarted == null || (DateTime.Now - _timerStarted) > _timerMaxLength) - { - return false; - } - - UpdateEnabled(); - return true; - }); - - base.OnAppearing(); - } - - protected override void OnDisappearing() - { - _timerStarted = null; - base.OnDisappearing(); - } - - private void UpdateEnabled() - { - ScrollView.Content = _appInfoService.AutofillServiceEnabled ? EnabledStackLayout : DisabledStackLayout; - } - - private Label BuildServiceLabel() - { - return new Label - { - Text = AppResources.AutofillServiceDescription, - VerticalOptions = LayoutOptions.Start, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)) - }; - } - } -} diff --git a/src/App/Pages/Tools/ToolsExtensionPage.cs b/src/App/Pages/Tools/ToolsExtensionPage.cs deleted file mode 100644 index 87cdbbff7..000000000 --- a/src/App/Pages/Tools/ToolsExtensionPage.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Plugin.Settings.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Abstractions; -using Bit.App.Resources; -using FFImageLoading.Forms; - -namespace Bit.App.Pages -{ - public class ToolsExtensionPage : ExtendedContentPage - { - private readonly ISettings _settings; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - - public ToolsExtensionPage() - { - _settings = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - Model = new AppExtensionPageModel(_settings); - - Init(); - } - - public AppExtensionPageModel Model { get; private set; } - - public void Init() - { - // Not Started - - var notStartedLabel = new Label - { - Text = AppResources.ExtensionInstantAccess, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) - }; - - var notStartedSublabel = new Label - { - Text = AppResources.ExtensionTurnOn, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap - }; - - var notStartedImage = new CachedImage - { - Source = "ext-more", - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, -10, 0, 0), - WidthRequest = 290, - HeightRequest = 252 - }; - - var notStartedButton = new ExtendedButton - { - Text = AppResources.ExtensionEnable, - Command = new Command(() => ShowExtension("NotStartedEnable")), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - - var notStartedStackLayout = new StackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 20, 20, 30), - Children = { notStartedLabel, notStartedSublabel, notStartedImage, notStartedButton }, - VerticalOptions = LayoutOptions.FillAndExpand - }; - - notStartedStackLayout.SetBinding(IsVisibleProperty, nameof(AppExtensionPageModel.NotStarted)); - - // Not Activated - - var notActivatedLabel = new Label - { - Text = AppResources.ExtensionAlmostDone, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) - }; - - var notActivatedSublabel = new Label - { - Text = AppResources.ExtensionTapIcon, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap - }; - - var notActivatedImage = new CachedImage - { - Source = "ext-act", - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, -10, 0, 0), - WidthRequest = 290, - HeightRequest = 252 - }; - - var notActivatedButton = new ExtendedButton - { - Text = AppResources.ExtensionEnable, - Command = new Command(() => ShowExtension("NotActivatedEnable")), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - - var notActivatedStackLayout = new StackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 20, - Padding = new Thickness(20, 20, 20, 30), - Children = { notActivatedLabel, notActivatedSublabel, notActivatedImage, notActivatedButton }, - VerticalOptions = LayoutOptions.FillAndExpand - }; - - notActivatedStackLayout.SetBinding(IsVisibleProperty, nameof(AppExtensionPageModel.StartedAndNotActivated)); - - // Activated - - var activatedLabel = new Label - { - Text = AppResources.ExtensionReady, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) - }; - - var activatedSublabel = new Label - { - Text = AppResources.ExtensionInSafari, - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Center, - HorizontalTextAlignment = TextAlignment.Center, - LineBreakMode = LineBreakMode.WordWrap, - Margin = new Thickness(0, 10, 0, 0) - }; - - var activatedImage = new CachedImage - { - Source = "ext-use", - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center, - Margin = new Thickness(0, -10, 0, 0), - WidthRequest = 290, - HeightRequest = 252 - }; - - var activatedButton = new ExtendedButton - { - Text = AppResources.ExtensionSeeApps, - Command = new Command(() => - { - _googleAnalyticsService.TrackAppEvent("SeeSupportedApps"); - Device.OpenUri(new Uri("https://bitwarden.com/ios/")); - }), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primary"] - }; - - var activatedButtonReenable = new ExtendedButton - { - Text = AppResources.ExntesionReenable, - Command = new Command(() => ShowExtension("Re-enable")), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.Fill, - Style = (Style)Application.Current.Resources["btn-primaryAccent"] - }; - - var activatedStackLayout = new StackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 10, - Padding = new Thickness(20, 20, 20, 30), - VerticalOptions = LayoutOptions.FillAndExpand, - Children = { activatedLabel, activatedSublabel, activatedImage, activatedButton, activatedButtonReenable } - }; - - activatedStackLayout.SetBinding(IsVisibleProperty, nameof(AppExtensionPageModel.StartedAndActivated)); - - var stackLayout = new StackLayout - { - Children = { notStartedStackLayout, notActivatedStackLayout, activatedStackLayout }, - VerticalOptions = LayoutOptions.FillAndExpand - }; - - if(Device.RuntimePlatform == Device.iOS) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - - Title = AppResources.AppExtension; - Content = new ScrollView { Content = stackLayout }; - BindingContext = Model; - } - - private void ShowExtension(string type) - { - _googleAnalyticsService.TrackAppEvent("ShowExtension", type); - MessagingCenter.Send(Application.Current, "ShowAppExtension", this); - } - - public void EnabledExtension(bool enabled) - { - _googleAnalyticsService.TrackAppEvent("EnabledExtension", enabled.ToString()); - Model.Started = true; - if(!Model.Activated && enabled) - { - Model.Activated = enabled; - } - } - } -} diff --git a/src/App/Pages/Tools/ToolsPage.cs b/src/App/Pages/Tools/ToolsPage.cs deleted file mode 100644 index 5826b1f28..000000000 --- a/src/App/Pages/Tools/ToolsPage.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using FFImageLoading.Forms; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class ToolsPage : ExtendedContentPage - { - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly IDeviceInfoService _deviceInfoService; - private readonly MainPage _mainPage; - - public ToolsPage(MainPage mainPage) - { - _mainPage = mainPage; - _googleAnalyticsService = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - - Init(); - } - - public ToolsViewCell WebCell { get; set; } - public ToolsViewCell ShareCell { get; set; } - public ToolsViewCell ImportCell { get; set; } - public ToolsViewCell ExtensionCell { get; set; } - public ToolsViewCell AutofillCell { get; set; } - public ToolsViewCell AccessibilityCell { get; set; } - - public void Init() - { - WebCell = new ToolsViewCell(AppResources.WebVault, AppResources.WebVaultDescription, "globe.png"); - ShareCell = new ToolsViewCell(AppResources.ShareVault, AppResources.ShareVaultDescription, "share_tools.png"); - ImportCell = new ToolsViewCell(AppResources.ImportItems, AppResources.ImportItemsDescription, "cloudup.png"); - - var section = new TableSection(Helpers.GetEmptyTableSectionTitle()); - - if(Device.RuntimePlatform == Device.iOS) - { - if(_deviceInfoService.Version < 12) - { - ExtensionCell = new ToolsViewCell(AppResources.BitwardenAppExtension, - AppResources.BitwardenAppExtensionDescription, "upload.png"); - section.Add(ExtensionCell); - } - else - { - ExtensionCell = new ToolsViewCell(AppResources.PasswordAutofill, - AppResources.BitwardenAutofillDescription, "magic.png"); - section.Add(ExtensionCell); - } - } - if(Device.RuntimePlatform == Device.Android) - { - var accessibilityDesc = AppResources.BitwardenAutofillAccessibilityServiceDescription; - if(_deviceInfoService.AutofillServiceSupported) - { - AutofillCell = new ToolsViewCell(AppResources.AutofillService, - AppResources.BitwardenAutofillServiceDescription, "upload2.png"); - section.Add(AutofillCell); - accessibilityDesc += (" " + AppResources.BitwardenAutofillAccessibilityServiceDescription2); - } - AccessibilityCell = new ToolsViewCell(AppResources.AutofillAccessibilityService, - accessibilityDesc, "upload.png"); - section.Add(AccessibilityCell); - } - - section.Add(WebCell); - section.Add(ShareCell); - section.Add(ImportCell); - - var table = new ExtendedTableView - { - EnableScrolling = true, - Intent = TableIntent.Settings, - HasUnevenRows = true, - Root = new TableRoot - { - section - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 100; - } - else if(Device.RuntimePlatform == Device.Android) - { - table.BottomPadding = 50; - } - - Title = AppResources.Tools; - Content = table; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - WebCell.Tapped += WebCell_Tapped; - ShareCell.Tapped += ShareCell_Tapped; - ImportCell.Tapped += ImportCell_Tapped; - if(ExtensionCell != null) - { - ExtensionCell.Tapped += ExtensionCell_Tapped; - } - if(AutofillCell != null) - { - AutofillCell.Tapped += AutofillCell_Tapped; - } - if(AccessibilityCell != null) - { - AccessibilityCell.Tapped += AccessibilityCell_Tapped; - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - WebCell.Tapped -= WebCell_Tapped; - ShareCell.Tapped -= ShareCell_Tapped; - ImportCell.Tapped -= ImportCell_Tapped; - if(ExtensionCell != null) - { - ExtensionCell.Tapped -= ExtensionCell_Tapped; - } - if(AutofillCell != null) - { - AutofillCell.Tapped -= AutofillCell_Tapped; - } - if(AccessibilityCell != null) - { - AccessibilityCell.Tapped -= AccessibilityCell_Tapped; - } - } - - protected override bool OnBackButtonPressed() - { - if(Device.RuntimePlatform == Device.Android && _mainPage != null) - { - _mainPage.ResetToVaultPage(); - return true; - } - - return base.OnBackButtonPressed(); - } - - private void AutofillCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAutofillServicePage())); - } - - private void AccessibilityCell_Tapped(object sender, EventArgs e) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAccessibilityServicePage())); - } - - private void ExtensionCell_Tapped(object sender, EventArgs e) - { - if(_deviceInfoService.Version < 12) - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsExtensionPage())); - } - else - { - Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAutofillPage())); - } - } - - private void WebCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("OpenedTool", "Web"); - var appSettings = Resolver.Resolve(); - if(!string.IsNullOrWhiteSpace(appSettings.BaseUrl)) - { - Device.OpenUri(new Uri(appSettings.BaseUrl)); - } - else - { - Device.OpenUri(new Uri("https://vault.bitwarden.com")); - } - } - - private void ShareCell_Tapped(object sender, EventArgs e) - { - _googleAnalyticsService.TrackAppEvent("OpenedTool", "Share"); - Device.OpenUri(new Uri("https://vault.bitwarden.com/#/?org=free")); - } - - private async void ImportCell_Tapped(object sender, EventArgs e) - { - var confirmed = await DisplayAlert(null, AppResources.ImportItemsConfirmation, AppResources.Yes, - AppResources.Cancel); - if(!confirmed) - { - return; - } - - _googleAnalyticsService.TrackAppEvent("OpenedTool", "Import"); - Device.OpenUri(new Uri("https://help.bitwarden.com/article/import-data/")); - } - - public class ToolsViewCell : ExtendedViewCell - { - public ToolsViewCell(string labelText, string detailText, string imageSource) - { - var label = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - LineBreakMode = LineBreakMode.TailTruncation, - Text = labelText - }; - - if(Device.RuntimePlatform == Device.Android) - { - label.TextColor = Color.Black; - } - - var detail = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - LineBreakMode = LineBreakMode.WordWrap, - Style = (Style)Application.Current.Resources["text-muted"], - Text = detailText - }; - - var image = new CachedImage - { - Source = imageSource, - WidthRequest = 44, - HeightRequest = 44 - }; - - var grid = new Grid - { - ColumnSpacing = 15, - RowSpacing = 0, - Padding = new Thickness(15, 20) - }; - grid.AdjustPaddingForDevice(); - - grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) }); - grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) }); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(44, GridUnitType.Absolute) }); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - grid.Children.Add(image, 0, 0); - Grid.SetRowSpan(image, 2); - grid.Children.Add(label, 1, 0); - grid.Children.Add(detail, 1, 1); - - ShowDisclousure = true; - View = grid; - } - } - } -} diff --git a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs b/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs deleted file mode 100644 index 36247bef0..000000000 --- a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs +++ /dev/null @@ -1,459 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Plugin.Settings.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; - -namespace Bit.App.Pages -{ - public class ToolsPasswordGeneratorPage : ExtendedContentPage - { - private readonly IPasswordGenerationService _passwordGenerationService; - private readonly ISettings _settings; - private readonly IDeviceActionService _deviceActionService; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly Action _passwordValueAction; - private readonly bool _fromAutofill; - private readonly MainPage _mainPage; - - public ToolsPasswordGeneratorPage(Action passwordValueAction = null, bool fromAutofill = false) - { - _passwordGenerationService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _passwordValueAction = passwordValueAction; - _fromAutofill = fromAutofill; - - Init(); - } - - public ToolsPasswordGeneratorPage(MainPage mainPage) - : this() - { - _mainPage = mainPage; - } - - public PasswordGeneratorPageModel Model { get; private set; } = new PasswordGeneratorPageModel(); - public Label Password { get; private set; } - public SliderViewCell SliderCell { get; private set; } - public TapGestureRecognizer Tgr { get; set; } - public ExtendedTextCell RegenerateCell { get; set; } - public ExtendedTextCell CopyCell { get; set; } - public ExtendedSwitchCell UppercaseCell { get; set; } - public ExtendedSwitchCell LowercaseCell { get; set; } - public ExtendedSwitchCell SpecialCell { get; set; } - public ExtendedSwitchCell NumbersCell { get; set; } - public ExtendedSwitchCell AvoidAmbiguousCell { get; set; } - public StepperCell SpecialMinCell { get; set; } - public StepperCell NumbersMinCell { get; set; } - - public void Init() - { - Password = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), - Margin = new Thickness(15, 30, 15, 30), - HorizontalTextAlignment = TextAlignment.Center, - FontFamily = Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"), - LineBreakMode = LineBreakMode.TailTruncation, - VerticalOptions = LayoutOptions.Start, - TextColor = Color.Black - }; - - Tgr = new TapGestureRecognizer(); - Password.GestureRecognizers.Add(Tgr); - // Password.SetBinding(Label.FormattedTextProperty, nameof(PasswordGeneratorPageModel.FormattedPassword)); - Password.SetBinding(Label.TextProperty, nameof(PasswordGeneratorPageModel.Password)); - - SliderCell = new SliderViewCell(this, _passwordGenerationService, _settings); - - RegenerateCell = new ExtendedTextCell - { - Text = AppResources.RegeneratePassword, - TextColor = Colors.Primary - }; - CopyCell = new ExtendedTextCell { Text = AppResources.CopyPassword, TextColor = Colors.Primary }; - - UppercaseCell = new ExtendedSwitchCell - { - Text = "A-Z", - On = _settings.GetValueOrDefault(Constants.PasswordGeneratorUppercase, true) - }; - - LowercaseCell = new ExtendedSwitchCell - { - Text = "a-z", - On = _settings.GetValueOrDefault(Constants.PasswordGeneratorLowercase, true) - }; - - SpecialCell = new ExtendedSwitchCell - { - Text = "!@#$%^&*", - On = _settings.GetValueOrDefault(Constants.PasswordGeneratorSpecial, true) - }; - - NumbersCell = new ExtendedSwitchCell - { - Text = "0-9", - On = _settings.GetValueOrDefault(Constants.PasswordGeneratorNumbers, true) - }; - - AvoidAmbiguousCell = new ExtendedSwitchCell - { - Text = AppResources.AvoidAmbiguousCharacters, - On = !_settings.GetValueOrDefault(Constants.PasswordGeneratorAmbiguous, false) - }; - - NumbersMinCell = new StepperCell(AppResources.MinNumbers, - _settings.GetValueOrDefault(Constants.PasswordGeneratorMinNumbers, 1), 0, 5, 1, () => - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorMinNumbers, - Convert.ToInt32(NumbersMinCell.Stepper.Value)); - Model.Password = _passwordGenerationService.GeneratePassword(); - }); - - SpecialMinCell = new StepperCell(AppResources.MinSpecial, - _settings.GetValueOrDefault(Constants.PasswordGeneratorMinSpecial, 1), 0, 5, 1, () => - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorMinSpecial, - Convert.ToInt32(SpecialMinCell.Stepper.Value)); - Model.Password = _passwordGenerationService.GeneratePassword(); - }); - - var table = new ExtendedTableView - { - VerticalOptions = LayoutOptions.Start, - EnableScrolling = false, - Intent = TableIntent.Settings, - HasUnevenRows = true, - NoHeader = true, - Root = new TableRoot - { - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - RegenerateCell, - CopyCell - }, - new TableSection(AppResources.Options) - { - SliderCell, - UppercaseCell, - LowercaseCell, - NumbersCell, - SpecialCell - }, - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - NumbersMinCell, - SpecialMinCell - }, - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - AvoidAmbiguousCell - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 44; - - if(_passwordValueAction != null) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - } - else if(Device.RuntimePlatform == Device.Android) - { - table.BottomPadding = 50; - } - - var stackLayout = new RedrawableStackLayout - { - Orientation = StackOrientation.Vertical, - Children = { Password, table }, - VerticalOptions = LayoutOptions.FillAndExpand, - Spacing = 0 - }; - - table.WrappingStackLayout = () => stackLayout; - - var scrollView = new ScrollView - { - Content = stackLayout, - Orientation = ScrollOrientation.Vertical, - VerticalOptions = LayoutOptions.FillAndExpand - }; - - if(_passwordValueAction != null) - { - var selectToolBarItem = new ToolbarItem(AppResources.Select, Helpers.ToolbarImage("ion_chevron_right.png"), async () => - { - if(_fromAutofill) - { - _googleAnalyticsService.TrackExtensionEvent("SelectedGeneratedPassword"); - } - else - { - _googleAnalyticsService.TrackAppEvent("SelectedGeneratedPassword"); - } - - _passwordValueAction(Model.Password); - await Navigation.PopForDeviceAsync(); - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(selectToolBarItem); - } - - Title = AppResources.PasswordGenerator; - Content = scrollView; - BindingContext = Model; - } - - private void Tgr_Tapped(object sender, EventArgs e) - { - CopyPassword(); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - Tgr.Tapped += Tgr_Tapped; - RegenerateCell.Tapped += RegenerateCell_Tapped; - CopyCell.Tapped += CopyCell_Tapped; - SliderCell.InitEvents(); - SpecialCell.OnChanged += SpecialCell_OnChanged; - AvoidAmbiguousCell.OnChanged += AvoidAmbiguousCell_OnChanged; - UppercaseCell.OnChanged += UppercaseCell_OnChanged; - LowercaseCell.OnChanged += LowercaseCell_OnChanged; - NumbersCell.OnChanged += NumbersCell_OnChanged; - NumbersMinCell.InitEvents(); - SpecialMinCell.InitEvents(); - - if(_fromAutofill) - { - _googleAnalyticsService.TrackExtensionEvent("GeneratedPassword"); - } - else - { - _googleAnalyticsService.TrackAppEvent("GeneratedPassword"); - } - Model.Password = _passwordGenerationService.GeneratePassword(); - Model.Length = _settings.GetValueOrDefault(Constants.PasswordGeneratorLength, 10).ToString(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - Tgr.Tapped -= Tgr_Tapped; - RegenerateCell.Tapped -= RegenerateCell_Tapped; - SpecialCell.OnChanged -= SpecialCell_OnChanged; - AvoidAmbiguousCell.OnChanged -= AvoidAmbiguousCell_OnChanged; - UppercaseCell.OnChanged -= UppercaseCell_OnChanged; - LowercaseCell.OnChanged -= LowercaseCell_OnChanged; - NumbersCell.OnChanged -= NumbersCell_OnChanged; - NumbersMinCell.Dispose(); - SpecialMinCell.Dispose(); - CopyCell.Tapped -= CopyCell_Tapped; - SliderCell.Dispose(); - } - - protected override bool OnBackButtonPressed() - { - if(Device.RuntimePlatform == Device.Android && _mainPage != null) - { - _mainPage.ResetToVaultPage(); - return true; - } - - return base.OnBackButtonPressed(); - } - - private void RegenerateCell_Tapped(object sender, EventArgs e) - { - Model.Password = _passwordGenerationService.GeneratePassword(); - if(_fromAutofill) - { - _googleAnalyticsService.TrackExtensionEvent("RegeneratedPassword"); - } - else - { - _googleAnalyticsService.TrackAppEvent("RegeneratedPassword"); - } - } - - private void CopyCell_Tapped(object sender, EventArgs e) - { - CopyPassword(); - } - - private void CopyPassword() - { - if(_fromAutofill) - { - _googleAnalyticsService.TrackExtensionEvent("CopiedGeneratedPassword"); - } - else - { - _googleAnalyticsService.TrackAppEvent("CopiedGeneratedPassword"); - } - _deviceActionService.CopyToClipboard(Model.Password); - _deviceActionService.Toast(string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); - } - - private void AvoidAmbiguousCell_OnChanged(object sender, ToggledEventArgs e) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorAmbiguous, !AvoidAmbiguousCell.On); - Model.Password = _passwordGenerationService.GeneratePassword(); - } - - private void NumbersCell_OnChanged(object sender, ToggledEventArgs e) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorNumbers, NumbersCell.On); - - if(InvalidState()) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorLowercase, true); - LowercaseCell.On = true; - } - - Model.Password = _passwordGenerationService.GeneratePassword(); - } - - private void SpecialCell_OnChanged(object sender, ToggledEventArgs e) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorSpecial, SpecialCell.On); - - if(InvalidState()) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorLowercase, true); - LowercaseCell.On = true; - } - - Model.Password = _passwordGenerationService.GeneratePassword(); - } - - private void LowercaseCell_OnChanged(object sender, ToggledEventArgs e) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorLowercase, LowercaseCell.On); - - if(InvalidState()) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorUppercase, true); - UppercaseCell.On = true; - } - - Model.Password = _passwordGenerationService.GeneratePassword(); - } - - private void UppercaseCell_OnChanged(object sender, ToggledEventArgs e) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorUppercase, UppercaseCell.On); - - if(InvalidState()) - { - _settings.AddOrUpdateValue(Constants.PasswordGeneratorLowercase, true); - LowercaseCell.On = true; - } - - Model.Password = _passwordGenerationService.GeneratePassword(); - } - - private bool InvalidState() - { - return !LowercaseCell.On && !UppercaseCell.On && !NumbersCell.On && !SpecialCell.On; - } - - - // TODO: move to standalone reusable control - public class SliderViewCell : ExtendedViewCell, IDisposable - { - private readonly ToolsPasswordGeneratorPage _page; - private readonly IPasswordGenerationService _passwordGenerationService; - private readonly ISettings _settings; - - public Label Value { get; set; } - public Slider LengthSlider { get; set; } - - public SliderViewCell( - ToolsPasswordGeneratorPage page, - IPasswordGenerationService passwordGenerationService, - ISettings settings) - { - _page = page; - _passwordGenerationService = passwordGenerationService; - _settings = settings; - - var label = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - Text = AppResources.Length, - HorizontalOptions = LayoutOptions.Start, - VerticalOptions = LayoutOptions.CenterAndExpand - }; - - LengthSlider = new Slider(5, 64, _settings.GetValueOrDefault(Constants.PasswordGeneratorLength, 10)) - { - HorizontalOptions = LayoutOptions.FillAndExpand, - VerticalOptions = LayoutOptions.CenterAndExpand, - MaximumTrackColor = Color.LightGray, - MinimumTrackColor = Color.LightGray, - }; - - Value = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - HorizontalOptions = LayoutOptions.End, - VerticalOptions = LayoutOptions.CenterAndExpand, - Style = (Style)Application.Current.Resources["text-muted"], - FontFamily = Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"), - }; - - Value.SetBinding(Label.TextProperty, nameof(PasswordGeneratorPageModel.Length)); - - var stackLayout = new StackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 15, - Children = { label, LengthSlider, Value }, - Padding = Helpers.OnPlatform( - iOS: new Thickness(15, 8), - Android: new Thickness(16, 10), - Windows: new Thickness(15, 8)) - }; - - stackLayout.AdjustPaddingForDevice(); - if(Device.RuntimePlatform == Device.Android) - { - label.TextColor = Color.Black; - } - - View = stackLayout; - } - - private void Slider_ValueChanged(object sender, ValueChangedEventArgs e) - { - var length = Convert.ToInt32(LengthSlider.Value); - _settings.AddOrUpdateValue(Constants.PasswordGeneratorLength, length); - _page.Model.Length = length.ToString(); - _page.Model.Password = _passwordGenerationService.GeneratePassword(); - } - - public void InitEvents() - { - LengthSlider.ValueChanged += Slider_ValueChanged; - } - - public void Dispose() - { - LengthSlider.ValueChanged -= Slider_ValueChanged; - } - } - } -} diff --git a/src/App/Pages/Vault/AddEditPage.xaml b/src/App/Pages/Vault/AddEditPage.xaml new file mode 100644 index 000000000..8df6d74fa --- /dev/null +++ b/src/App/Pages/Vault/AddEditPage.xaml @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/AddEditPage.xaml.cs b/src/App/Pages/Vault/AddEditPage.xaml.cs new file mode 100644 index 000000000..5d23b056f --- /dev/null +++ b/src/App/Pages/Vault/AddEditPage.xaml.cs @@ -0,0 +1,177 @@ +using Bit.App.Models; +using Bit.Core.Enums; +using System.Collections.Generic; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class AddEditPage : BaseContentPage + { + private AddEditPageViewModel _vm; + private readonly AppOptions _appOptions; + private bool _fromAutofill; + + public AddEditPage( + string cipherId = null, + CipherType? type = null, + string folderId = null, + string collectionId = null, + string name = null, + string uri = null, + bool fromAutofill = false, + AppOptions appOptions = null) + { + _appOptions = appOptions; + _fromAutofill = fromAutofill; + FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false; + InitializeComponent(); + _vm = BindingContext as AddEditPageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + _vm.FolderId = folderId; + _vm.CollectionIds = collectionId != null ? new HashSet(new List { collectionId }) : null; + _vm.Type = type; + _vm.DefaultName = name ?? appOptions?.SaveName; + _vm.DefaultUri = uri ?? appOptions?.Uri; + _vm.Init(); + SetActivityIndicator(); + if(!_vm.EditMode) + { + ToolbarItems.Remove(_attachmentsItem); + ToolbarItems.Remove(_deleteItem); + } + + _typePicker.ItemDisplayBinding = new Binding("Key"); + _cardBrandPicker.ItemDisplayBinding = new Binding("Key"); + _cardExpMonthPicker.ItemDisplayBinding = new Binding("Key"); + _identityTitlePicker.ItemDisplayBinding = new Binding("Key"); + _folderPicker.ItemDisplayBinding = new Binding("Key"); + _ownershipPicker.ItemDisplayBinding = new Binding("Key"); + } + + public bool FromAutofillFramework { get; set; } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync(_appOptions)); + if(_vm.EditMode && Device.RuntimePlatform == Device.Android) + { + if(_vm.Cipher.OrganizationId == null) + { + if(ToolbarItems.Contains(_collectionsItem)) + { + ToolbarItems.Remove(_collectionsItem); + } + if(!ToolbarItems.Contains(_shareItem)) + { + ToolbarItems.Insert(2, _shareItem); + } + } + else + { + if(ToolbarItems.Contains(_shareItem)) + { + ToolbarItems.Remove(_shareItem); + } + if(!ToolbarItems.Contains(_collectionsItem)) + { + ToolbarItems.Insert(2, _collectionsItem); + } + } + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + } + + protected override bool OnBackButtonPressed() + { + if(FromAutofillFramework) + { + Application.Current.MainPage = new TabsPage(); + return true; + } + return base.OnBackButtonPressed(); + } + + private async void PasswordHistory_Tapped(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await Navigation.PushModalAsync(new NavigationPage(new PasswordHistoryPage(_vm.CipherId))); + } + } + + private async void Save_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private void NewUri_Clicked(object sender, System.EventArgs e) + { + _vm.AddUri(); + } + + private void NewField_Clicked(object sender, System.EventArgs e) + { + _vm.AddField(); + } + + private async void Attachments_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new AttachmentsPage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + + private async void Share_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new SharePage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + + private async void Delete_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.DeleteAsync(); + } + } + + private async void Collections_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new CollectionsPage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + + private async void ScanTotp_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new ScanPage(key => + { + Device.BeginInvokeOnMainThread(async () => + { + await Navigation.PopModalAsync(); + await _vm.UpdateTotpKeyAsync(key); + }); + }); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + } +} diff --git a/src/App/Pages/Vault/AddEditPageViewModel.cs b/src/App/Pages/Vault/AddEditPageViewModel.cs new file mode 100644 index 000000000..c4644f11e --- /dev/null +++ b/src/App/Pages/Vault/AddEditPageViewModel.cs @@ -0,0 +1,752 @@ +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class AddEditPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + private readonly IFolderService _folderService; + private readonly ICollectionService _collectionService; + private readonly IUserService _userService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IAuditService _auditService; + private readonly IMessagingService _messagingService; + private CipherView _cipher; + private bool _showPassword; + private bool _showCardCode; + private int _typeSelectedIndex; + private int _cardBrandSelectedIndex; + private int _cardExpMonthSelectedIndex; + private int _identityTitleSelectedIndex; + private int _folderSelectedIndex; + private int _ownershipSelectedIndex; + private bool _hasCollections; + private List _writeableCollections; + private string[] _additionalCipherProperties = new string[] + { + nameof(IsLogin), + nameof(IsIdentity), + nameof(IsCard), + nameof(IsSecureNote), + nameof(ShowUris), + nameof(ShowAttachments), + nameof(ShowCollections), + }; + private List> _matchDetectionOptions = + new List> + { + new KeyValuePair(null, AppResources.Default), + new KeyValuePair(UriMatchType.Domain, AppResources.BaseDomain), + new KeyValuePair(UriMatchType.Host, AppResources.Host), + new KeyValuePair(UriMatchType.StartsWith, AppResources.StartsWith), + new KeyValuePair(UriMatchType.RegularExpression, AppResources.RegEx), + new KeyValuePair(UriMatchType.Exact, AppResources.Exact), + new KeyValuePair(UriMatchType.Never, AppResources.Never) + }; + private List> _fieldTypeOptions = + new List> + { + new KeyValuePair(FieldType.Text, AppResources.FieldTypeText), + new KeyValuePair(FieldType.Hidden, AppResources.FieldTypeHidden), + new KeyValuePair(FieldType.Boolean, AppResources.FieldTypeBoolean) + }; + + public AddEditPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _folderService = ServiceContainer.Resolve("folderService"); + _userService = ServiceContainer.Resolve("userService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _auditService = ServiceContainer.Resolve("auditService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _collectionService = ServiceContainer.Resolve("collectionService"); + GeneratePasswordCommand = new Command(GeneratePassword); + TogglePasswordCommand = new Command(TogglePassword); + ToggleCardCodeCommand = new Command(ToggleCardCode); + CheckPasswordCommand = new Command(CheckPasswordAsync); + UriOptionsCommand = new Command(UriOptions); + FieldOptionsCommand = new Command(FieldOptions); + Uris = new ExtendedObservableCollection(); + Fields = new ExtendedObservableCollection(); + Collections = new ExtendedObservableCollection(); + + TypeOptions = new List> + { + new KeyValuePair(AppResources.TypeLogin, CipherType.Login), + new KeyValuePair(AppResources.TypeCard, CipherType.Card), + new KeyValuePair(AppResources.TypeIdentity, CipherType.Identity), + new KeyValuePair(AppResources.TypeSecureNote, CipherType.SecureNote), + }; + CardBrandOptions = new List> + { + new KeyValuePair($"-- {AppResources.Select} --", null), + new KeyValuePair("Visa", "Visa"), + new KeyValuePair("Mastercard", "Mastercard"), + new KeyValuePair("American Express", "Amex"), + new KeyValuePair("Discover", "Discover"), + new KeyValuePair("Diners Club", "Diners Club"), + new KeyValuePair("JCB", "JCB"), + new KeyValuePair("Maestro", "Maestro"), + new KeyValuePair("UnionPay", "UnionPay"), + new KeyValuePair(AppResources.Other, "Other") + }; + CardExpMonthOptions = new List> + { + new KeyValuePair($"-- {AppResources.Select} --", null), + new KeyValuePair($"01 - {AppResources.January}", "1"), + new KeyValuePair($"02 - {AppResources.February}", "2"), + new KeyValuePair($"03 - {AppResources.March}", "3"), + new KeyValuePair($"04 - {AppResources.April}", "4"), + new KeyValuePair($"05 - {AppResources.May}", "5"), + new KeyValuePair($"06 - {AppResources.June}", "6"), + new KeyValuePair($"07 - {AppResources.July}", "7"), + new KeyValuePair($"08 - {AppResources.August}", "8"), + new KeyValuePair($"09 - {AppResources.September}", "9"), + new KeyValuePair($"10 - {AppResources.October}", "10"), + new KeyValuePair($"11 - {AppResources.November}", "11"), + new KeyValuePair($"12 - {AppResources.December}", "12") + }; + IdentityTitleOptions = new List> + { + new KeyValuePair($"-- {AppResources.Select} --", null), + new KeyValuePair(AppResources.Mr, AppResources.Mr), + new KeyValuePair(AppResources.Mrs, AppResources.Mrs), + new KeyValuePair(AppResources.Ms, AppResources.Ms), + new KeyValuePair(AppResources.Dr, AppResources.Dr), + }; + FolderOptions = new List>(); + OwnershipOptions = new List>(); + } + + public Command GeneratePasswordCommand { get; set; } + public Command TogglePasswordCommand { get; set; } + public Command ToggleCardCodeCommand { get; set; } + public Command CheckPasswordCommand { get; set; } + public Command UriOptionsCommand { get; set; } + public Command FieldOptionsCommand { get; set; } + public string CipherId { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public CipherType? Type { get; set; } + public HashSet CollectionIds { get; set; } + public string DefaultName { get; set; } + public string DefaultUri { get; set; } + public List> TypeOptions { get; set; } + public List> CardBrandOptions { get; set; } + public List> CardExpMonthOptions { get; set; } + public List> IdentityTitleOptions { get; set; } + public List> FolderOptions { get; set; } + public List> OwnershipOptions { get; set; } + public ExtendedObservableCollection Uris { get; set; } + public ExtendedObservableCollection Fields { get; set; } + public ExtendedObservableCollection Collections { get; set; } + public int TypeSelectedIndex + { + get => _typeSelectedIndex; + set + { + if(SetProperty(ref _typeSelectedIndex, value)) + { + TypeChanged(); + } + } + } + public int CardBrandSelectedIndex + { + get => _cardBrandSelectedIndex; + set + { + if(SetProperty(ref _cardBrandSelectedIndex, value)) + { + CardBrandChanged(); + } + } + } + public int CardExpMonthSelectedIndex + { + get => _cardExpMonthSelectedIndex; + set + { + if(SetProperty(ref _cardExpMonthSelectedIndex, value)) + { + CardExpMonthChanged(); + } + } + } + public int IdentityTitleSelectedIndex + { + get => _identityTitleSelectedIndex; + set + { + if(SetProperty(ref _identityTitleSelectedIndex, value)) + { + IdentityTitleChanged(); + } + } + } + public int FolderSelectedIndex + { + get => _folderSelectedIndex; + set + { + if(SetProperty(ref _folderSelectedIndex, value)) + { + FolderChanged(); + } + } + } + public int OwnershipSelectedIndex + { + get => _ownershipSelectedIndex; + set + { + if(SetProperty(ref _ownershipSelectedIndex, value)) + { + OrganizationChanged(); + } + } + } + public CipherView Cipher + { + get => _cipher; + set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties); + } + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + public bool ShowCardCode + { + get => _showCardCode; + set => SetProperty(ref _showCardCode, value, + additionalPropertyNames: new string[] + { + nameof(ShowCardCodeIcon) + }); + } + public bool HasCollections + { + get => _hasCollections; + set => SetProperty(ref _hasCollections, value, + additionalPropertyNames: new string[] + { + nameof(ShowCollections) + }); + } + public bool ShowCollections => !EditMode && Cipher.OrganizationId != null; + public bool EditMode => !string.IsNullOrWhiteSpace(CipherId); + public bool IsLogin => Cipher?.Type == CipherType.Login; + public bool IsIdentity => Cipher?.Type == CipherType.Identity; + public bool IsCard => Cipher?.Type == CipherType.Card; + public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote; + public bool ShowUris => IsLogin && Cipher.Login.HasUris; + public bool ShowAttachments => Cipher.HasAttachments; + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string ShowCardCodeIcon => ShowCardCode ? "" : ""; + + public void Init() + { + PageTitle = EditMode ? AppResources.EditItem : AppResources.AddItem; + } + + public async Task LoadAsync(AppOptions appOptions = null) + { + var myEmail = await _userService.GetEmailAsync(); + OwnershipOptions.Add(new KeyValuePair(myEmail, null)); + var orgs = await _userService.GetAllOrganizationAsync(); + foreach(var org in orgs.OrderBy(o => o.Name)) + { + if(org.Enabled && org.Status == OrganizationUserStatusType.Confirmed) + { + OwnershipOptions.Add(new KeyValuePair(org.Name, org.Id)); + } + } + + var allCollections = await _collectionService.GetAllDecryptedAsync(); + _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); + if(CollectionIds?.Any() ?? false) + { + var colId = CollectionIds.First(); + var collection = _writeableCollections.FirstOrDefault(c => c.Id == colId); + OrganizationId = collection?.OrganizationId; + } + var folders = await _folderService.GetAllDecryptedAsync(); + FolderOptions = folders.Select(f => new KeyValuePair(f.Name, f.Id)).ToList(); + + if(Cipher == null) + { + if(EditMode) + { + var cipher = await _cipherService.GetAsync(CipherId); + Cipher = await cipher.DecryptAsync(); + } + else + { + Cipher = new CipherView + { + Name = DefaultName, + OrganizationId = OrganizationId, + FolderId = FolderId, + Type = Type.GetValueOrDefault(CipherType.Login), + Login = new LoginView(), + Card = new CardView(), + Identity = new IdentityView(), + SecureNote = new SecureNoteView() + }; + Cipher.Login.Uris = new List { new LoginUriView { Uri = DefaultUri } }; + Cipher.SecureNote.Type = SecureNoteType.Generic; + TypeSelectedIndex = TypeOptions.FindIndex(k => k.Value == Cipher.Type); + + if(appOptions != null) + { + Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type); + Cipher.Login.Username = appOptions.SaveUsername; + Cipher.Login.Password = appOptions.SavePassword; + Cipher.Card.Code = appOptions.SaveCardCode; + if(int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1) + { + Cipher.Card.ExpMonth = month.ToString(); + } + Cipher.Card.ExpYear = appOptions.SaveCardExpYear; + Cipher.Card.CardholderName = appOptions.SaveCardName; + Cipher.Card.Number = appOptions.SaveCardNumber; + } + } + + FolderSelectedIndex = string.IsNullOrWhiteSpace(Cipher.FolderId) ? FolderOptions.Count - 1 : + FolderOptions.FindIndex(k => k.Value == Cipher.FolderId); + CardBrandSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.Brand) ? 0 : + CardBrandOptions.FindIndex(k => k.Value == Cipher.Card.Brand); + CardExpMonthSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.ExpMonth) ? 0 : + CardExpMonthOptions.FindIndex(k => k.Value == Cipher.Card.ExpMonth); + IdentityTitleSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Identity?.Title) ? 0 : + IdentityTitleOptions.FindIndex(k => k.Value == Cipher.Identity.Title); + OwnershipSelectedIndex = string.IsNullOrWhiteSpace(Cipher.OrganizationId) ? 0 : + OwnershipOptions.FindIndex(k => k.Value == Cipher.OrganizationId); + + if(!EditMode && (CollectionIds?.Any() ?? false)) + { + foreach(var col in Collections) + { + col.Checked = CollectionIds.Contains(col.Collection.Id); + } + } + if(Cipher.Login.Uris != null) + { + Uris.ResetWithRange(Cipher.Login.Uris); + } + if(Cipher.Fields != null) + { + Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(f))); + } + } + } + + public async Task SubmitAsync() + { + if(string.IsNullOrWhiteSpace(Cipher.Name)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.Name), + AppResources.Ok); + return false; + } + + Cipher.Fields = Fields.Any() ? Fields.Select(f => f.Field).ToList() : null; + Cipher.Login.Uris = Uris.ToList(); + if(!EditMode && Cipher.Type == CipherType.Login && (Cipher.Login.Uris?.Count ?? 0) == 1 && + string.IsNullOrWhiteSpace(Cipher.Login.Uris.First().Uri)) + { + Cipher.Login.Uris = null; + } + + if(!EditMode && Cipher.OrganizationId != null) + { + if(!Collections?.Any(c => c.Checked) ?? true) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, + AppResources.Ok); + return false; + } + + Cipher.CollectionIds = Collections.Any() ? + new HashSet(Collections.Where(c => c.Checked).Select(c => c.Collection.Id)) : null; + } + + var cipher = await _cipherService.EncryptAsync(Cipher); + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.SaveWithServerAsync(cipher); + Cipher.Id = cipher.Id; + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, + EditMode ? AppResources.ItemUpdated : AppResources.NewItemCreated); + _messagingService.Send(EditMode ? "editedCipher" : "addedCipher"); + + if((Page as AddEditPage).FromAutofillFramework) + { + // Close and go back to app + _deviceActionService.CloseAutofill(); + } + else + { + await Page.Navigation.PopModalAsync(); + } + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + + public async Task DeleteAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete, + null, AppResources.Yes, AppResources.No); + if(!confirmed) + { + return false; + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); + await _cipherService.DeleteWithServerAsync(Cipher.Id); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted); + _messagingService.Send("deletedCipher"); + await Page.Navigation.PopModalAsync(); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + + public async void GeneratePassword() + { + if(!string.IsNullOrWhiteSpace(Cipher?.Login?.Password)) + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.PasswordOverrideAlert, + null, AppResources.Yes, AppResources.No); + if(!confirmed) + { + return; + } + } + var page = new GeneratorPage(false, async (password) => + { + Cipher.Login.Password = password; + TriggerCipherChanged(); + await Page.Navigation.PopModalAsync(); + }); + await Page.Navigation.PushModalAsync(new NavigationPage(page)); + } + + public async void UriOptions(LoginUriView uri) + { + if(!(Page as AddEditPage).DoOnce()) + { + return; + } + var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, + AppResources.MatchDetection, AppResources.Remove); + if(selection == AppResources.Remove) + { + Uris.Remove(uri); + } + else if(selection == AppResources.MatchDetection) + { + var options = _matchDetectionOptions.Select(o => o.Key == uri.Match ? $"✓ {o.Value}" : o.Value); + var matchSelection = await Page.DisplayActionSheet(AppResources.URIMatchDetection, + AppResources.Cancel, null, options.ToArray()); + if(matchSelection != null && matchSelection != AppResources.Cancel) + { + var matchSelectionClean = matchSelection.Replace("✓ ", string.Empty); + uri.Match = _matchDetectionOptions.FirstOrDefault(o => o.Value == matchSelectionClean).Key; + } + } + } + + public void AddUri() + { + if(Cipher.Type != CipherType.Login) + { + return; + } + if(Uris == null) + { + Uris = new ExtendedObservableCollection(); + } + Uris.Add(new LoginUriView()); + } + + public async void FieldOptions(AddEditPageFieldViewModel field) + { + if(!(Page as AddEditPage).DoOnce()) + { + return; + } + var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, + AppResources.Edit, AppResources.MoveUp, AppResources.MoveDown, AppResources.Remove); + if(selection == AppResources.Remove) + { + Fields.Remove(field); + } + else if(selection == AppResources.Edit) + { + var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName, + null, field.Field.Name); + field.Field.Name = name ?? field.Field.Name; + field.TriggerFieldChanged(); + } + else if(selection == AppResources.MoveUp) + { + var currentIndex = Fields.IndexOf(field); + if(currentIndex > 0) + { + Fields.Move(currentIndex, currentIndex - 1); + } + } + else if(selection == AppResources.MoveDown) + { + var currentIndex = Fields.IndexOf(field); + if(currentIndex < Fields.Count - 1) + { + Fields.Move(currentIndex, currentIndex + 1); + } + } + } + + public async void AddField() + { + var typeSelection = await Page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null, + _fieldTypeOptions.Select(f => f.Value).ToArray()); + if(typeSelection != null && typeSelection != AppResources.Cancel) + { + var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName); + if(name == null) + { + return; + } + if(Fields == null) + { + Fields = new ExtendedObservableCollection(); + } + var type = _fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key; + Fields.Add(new AddEditPageFieldViewModel(new FieldView + { + Type = type, + Name = string.IsNullOrWhiteSpace(name) ? null : name + })); + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + } + + public void ToggleCardCode() + { + ShowCardCode = !ShowCardCode; + } + + public async Task UpdateTotpKeyAsync(string key) + { + if(Cipher?.Login != null) + { + if(!string.IsNullOrWhiteSpace(key)) + { + Cipher.Login.Totp = key; + TriggerCipherChanged(); + _platformUtilsService.ShowToast("info", null, AppResources.AuthenticatorKeyAdded); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.AuthenticatorKeyReadError); + } + } + } + + private void TypeChanged() + { + if(Cipher != null && TypeSelectedIndex > -1) + { + Cipher.Type = TypeOptions[TypeSelectedIndex].Value; + TriggerCipherChanged(); + } + } + + private void CardBrandChanged() + { + if(Cipher?.Card != null && CardBrandSelectedIndex > -1) + { + Cipher.Card.Brand = CardBrandOptions[CardBrandSelectedIndex].Value; + } + } + + private void CardExpMonthChanged() + { + if(Cipher?.Card != null && CardExpMonthSelectedIndex > -1) + { + Cipher.Card.ExpMonth = CardExpMonthOptions[CardExpMonthSelectedIndex].Value; + } + } + + private void IdentityTitleChanged() + { + if(Cipher?.Identity != null && IdentityTitleSelectedIndex > -1) + { + Cipher.Identity.Title = IdentityTitleOptions[IdentityTitleSelectedIndex].Value; + } + } + + private void FolderChanged() + { + if(Cipher != null && FolderSelectedIndex > -1) + { + Cipher.FolderId = FolderOptions[FolderSelectedIndex].Value; + } + } + + private void OrganizationChanged() + { + if(Cipher != null && OwnershipSelectedIndex > -1) + { + Cipher.OrganizationId = OwnershipOptions[OwnershipSelectedIndex].Value; + TriggerCipherChanged(); + } + if(Cipher.OrganizationId != null) + { + var cols = _writeableCollections.Where(c => c.OrganizationId == Cipher.OrganizationId) + .Select(c => new CollectionViewModel { Collection = c }).ToList(); + Collections.ResetWithRange(cols); + } + else + { + Collections.ResetWithRange(new List()); + } + HasCollections = Collections.Any(); + } + + private void TriggerCipherChanged() + { + TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties); + } + + private async void CheckPasswordAsync() + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + if(string.IsNullOrWhiteSpace(Cipher.Login?.Password)) + { + return; + } + await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword); + var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password); + await _deviceActionService.HideLoadingAsync(); + if(matches > 0) + { + await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed, + matches.ToString("N0"))); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe); + } + } + } + + public class AddEditPageFieldViewModel : ExtendedViewModel + { + private FieldView _field; + private bool _showHiddenValue; + private bool _booleanValue; + private string[] _additionalFieldProperties = new string[] + { + nameof(IsBooleanType), + nameof(IsHiddenType), + nameof(IsTextType), + }; + + public AddEditPageFieldViewModel(FieldView field) + { + Field = field; + ToggleHiddenValueCommand = new Command(ToggleHiddenValue); + BooleanValue = IsBooleanType && field.Value == "true"; + } + + public FieldView Field + { + get => _field; + set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties); + } + + public bool ShowHiddenValue + { + get => _showHiddenValue; + set => SetProperty(ref _showHiddenValue, value, + additionalPropertyNames: new string[] + { + nameof(ShowHiddenValueIcon) + }); + } + + public bool BooleanValue + { + get => _booleanValue; + set + { + SetProperty(ref _booleanValue, value); + if(IsBooleanType) + { + Field.Value = value ? "true" : "false"; + } + } + } + + public Command ToggleHiddenValueCommand { get; set; } + + public string ShowHiddenValueIcon => _showHiddenValue ? "" : ""; + public bool IsTextType => _field.Type == FieldType.Text; + public bool IsBooleanType => _field.Type == FieldType.Boolean; + public bool IsHiddenType => _field.Type == FieldType.Hidden; + + public void ToggleHiddenValue() + { + ShowHiddenValue = !ShowHiddenValue; + } + + public void TriggerFieldChanged() + { + TriggerPropertyChanged(nameof(Field), _additionalFieldProperties); + } + } +} diff --git a/src/App/Pages/Vault/AttachmentsPage.xaml b/src/App/Pages/Vault/AttachmentsPage.xaml new file mode 100644 index 000000000..d60a9ddbf --- /dev/null +++ b/src/App/Pages/Vault/AttachmentsPage.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/AttachmentsPage.xaml.cs b/src/App/Pages/Vault/AttachmentsPage.xaml.cs new file mode 100644 index 000000000..aa692bb14 --- /dev/null +++ b/src/App/Pages/Vault/AttachmentsPage.xaml.cs @@ -0,0 +1,60 @@ +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class AttachmentsPage : BaseContentPage + { + private AttachmentsPageViewModel _vm; + private readonly IBroadcasterService _broadcasterService; + + public AttachmentsPage(string cipherId) + { + InitializeComponent(); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _vm = BindingContext as AttachmentsPageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + SetActivityIndicator(); + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + _broadcasterService.Subscribe(nameof(AttachmentsPage), (message) => + { + if(message.Command == "selectFileResult") + { + var data = message.Data as Tuple; + _vm.FileData = data.Item1; + _vm.FileName = data.Item2; + } + }); + await LoadOnAppearedAsync(_scrollView, true, () => _vm.InitAsync()); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _broadcasterService.Unsubscribe(nameof(AttachmentsPage)); + } + + private async void Save_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void ChooseFile_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.ChooseFileAsync(); + } + } + } +} diff --git a/src/App/Pages/Vault/AttachmentsPageViewModel.cs b/src/App/Pages/Vault/AttachmentsPageViewModel.cs new file mode 100644 index 000000000..f7f241786 --- /dev/null +++ b/src/App/Pages/Vault/AttachmentsPageViewModel.cs @@ -0,0 +1,165 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class AttachmentsPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IPlatformUtilsService _platformUtilsService; + private CipherView _cipher; + private Cipher _cipherDomain; + private bool _hasAttachments; + private bool _hasUpdatedKey; + private bool _canAccessAttachments; + private string _fileName; + + public AttachmentsPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _userService = ServiceContainer.Resolve("userService"); + Attachments = new ExtendedObservableCollection(); + DeleteAttachmentCommand = new Command(DeleteAsync); + PageTitle = AppResources.Attachments; + } + + public string CipherId { get; set; } + public CipherView Cipher + { + get => _cipher; + set => SetProperty(ref _cipher, value); + } + public ExtendedObservableCollection Attachments { get; set; } + public bool HasAttachments + { + get => _hasAttachments; + set => SetProperty(ref _hasAttachments, value); + } + public string FileName + { + get => _fileName; + set => SetProperty(ref _fileName, value); + } + public byte[] FileData { get; set; } + public Command DeleteAttachmentCommand { get; set; } + + public async Task InitAsync() + { + _cipherDomain = await _cipherService.GetAsync(CipherId); + Cipher = await _cipherDomain.DecryptAsync(); + LoadAttachments(); + _hasUpdatedKey = await _cryptoService.HasEncKeyAsync(); + var canAccessPremium = await _userService.CanAccessPremiumAsync(); + _canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null; + if(!_canAccessAttachments) + { + await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired); + } + else if(!_hasUpdatedKey) + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey, + AppResources.FeatureUnavailable, AppResources.LearnMore, AppResources.Cancel); + if(confirmed) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/update-encryption-key/"); + } + } + } + + public async Task SubmitAsync() + { + if(!_hasUpdatedKey) + { + await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey, + AppResources.AnErrorHasOccurred); + return false; + } + if(FileData == null) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.File), + AppResources.AnErrorHasOccurred); + return false; + } + if(FileData.Length > 104857600) // 100 MB + { + await _platformUtilsService.ShowDialogAsync(AppResources.MaxFileSize, + AppResources.AnErrorHasOccurred); + return false; + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + _cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync( + _cipherDomain, FileName, FileData); + Cipher = await _cipherDomain.DecryptAsync(); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded); + LoadAttachments(); + FileData = null; + FileName = null; + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + + public async Task ChooseFileAsync() + { + await _deviceActionService.SelectFileAsync(); + } + + private async void DeleteAsync(AttachmentView attachment) + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete, + null, AppResources.Yes, AppResources.No); + if(!confirmed) + { + return; + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); + await _cipherService.DeleteAttachmentWithServerAsync(Cipher.Id, attachment.Id); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.AttachmentDeleted); + var attachmentToRemove = Cipher.Attachments.FirstOrDefault(a => a.Id == attachment.Id); + if(attachmentToRemove != null) + { + Cipher.Attachments.Remove(attachmentToRemove); + LoadAttachments(); + } + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + + private void LoadAttachments() + { + Attachments.ResetWithRange(Cipher.Attachments ?? new List()); + HasAttachments = Cipher.HasAttachments; + } + } +} diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml b/src/App/Pages/Vault/AutofillCiphersPage.xaml new file mode 100644 index 000000000..55b32e28b --- /dev/null +++ b/src/App/Pages/Vault/AutofillCiphersPage.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RecycleElement + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs new file mode 100644 index 000000000..17d7b5764 --- /dev/null +++ b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs @@ -0,0 +1,80 @@ +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class AutofillCiphersPage : BaseContentPage + { + private readonly AppOptions _appOptions; + private readonly IPlatformUtilsService _platformUtilsService; + + private AutofillCiphersPageViewModel _vm; + + public AutofillCiphersPage(AppOptions appOptions) + { + _appOptions = appOptions; + InitializeComponent(); + _vm = BindingContext as AutofillCiphersPageViewModel; + _vm.Page = this; + _fab.Clicked = AddButton_Clicked; + _vm.Init(appOptions); + + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, false, async () => + { + await _vm.LoadAsync(); + }, _mainContent); + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(!DoOnce()) + { + return; + } + if(e.SelectedItem is GroupingsPageListItem item && item.Cipher != null) + { + await _vm.SelectCipherAsync(item.Cipher, item.FuzzyAutofill); + } + } + + private async void AddButton_Clicked(object sender, System.EventArgs e) + { + if(!DoOnce()) + { + return; + } + if(_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login) + { + var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true); + await Navigation.PushModalAsync(new NavigationPage(pageForOther)); + return; + } + var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name, + fromAutofill: true); + await Navigation.PushModalAsync(new NavigationPage(pageForLogin)); + } + + private void Search_Clicked(object sender, System.EventArgs e) + { + var page = new CiphersPage(null, autofillUrl: _vm.Uri); + Application.Current.MainPage = new NavigationPage(page); + _platformUtilsService.ShowToast("info", null, + string.Format(AppResources.BitwardenAutofillServiceSearch, _vm.Name), + new System.Collections.Generic.Dictionary + { + ["longDuration"] = true + }); + } + } +} diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs new file mode 100644 index 000000000..fc57dd2da --- /dev/null +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -0,0 +1,164 @@ +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class AutofillCiphersPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + + private AppOptions _appOptions; + private bool _showList; + private string _noDataText; + + public AutofillCiphersPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + + GroupedItems = new ExtendedObservableCollection(); + CipherOptionsCommand = new Command(CipherOptionsAsync); + } + + public string Name { get; set; } + public string Uri { get; set; } + public Command CipherOptionsCommand { get; set; } + public ExtendedObservableCollection GroupedItems { get; set; } + + public bool ShowList + { + get => _showList; + set => SetProperty(ref _showList, value); + } + + public string NoDataText + { + get => _noDataText; + set => SetProperty(ref _noDataText, value); + } + + public void Init(AppOptions appOptions) + { + _appOptions = appOptions; + Uri = appOptions.Uri; + string name = null; + if(Uri.StartsWith(Constants.AndroidAppProtocol)) + { + name = Uri.Substring(Constants.AndroidAppProtocol.Length); + } + else if(!System.Uri.TryCreate(Uri, UriKind.Absolute, out Uri uri) || + !DomainName.TryParseBaseDomain(uri.Host, out name)) + { + name = "--"; + } + Name = name; + PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--"); + NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--"); + } + + public async Task LoadAsync() + { + ShowList = false; + var groupedItems = new List(); + var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null); + var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); + if(matching?.Any() ?? false) + { + groupedItems.Add( + new GroupingsPageListGroup(matching, AppResources.MatchingItems, matching.Count, false)); + } + var fuzzy = ciphers.Item2?.Select(c => + new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList(); + if(fuzzy?.Any() ?? false) + { + groupedItems.Add( + new GroupingsPageListGroup(fuzzy, AppResources.PossibleMatchingItems, fuzzy.Count, false)); + } + GroupedItems.ResetWithRange(groupedItems); + ShowList = groupedItems.Any(); + } + + public async Task SelectCipherAsync(CipherView cipher, bool fuzzy) + { + if(_deviceActionService.SystemMajorVersion() < 21) + { + // TODO + } + else + { + var autofillResponse = AppResources.Yes; + if(fuzzy) + { + var options = new List { AppResources.Yes }; + if(cipher.Type == CipherType.Login) + { + options.Add(AppResources.YesAndSave); + } + autofillResponse = await _deviceActionService.DisplayAlertAsync(null, + string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No, + options.ToArray()); + } + if(autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login) + { + var uris = cipher.Login?.Uris?.ToList(); + if(uris == null) + { + uris = new List(); + } + uris.Add(new LoginUriView + { + Uri = Uri, + Match = null + }); + cipher.Login.Uris = uris; + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher)); + await _deviceActionService.HideLoadingAsync(); + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), + AppResources.Ok); + } + } + if(autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave) + { + _deviceActionService.Autofill(cipher); + } + } + } + + private async void CipherOptionsAsync(CipherView cipher) + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + var option = await Page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, "1", "2"); + if(option == AppResources.Cancel) + { + return; + } + // TODO: process options + } + } +} diff --git a/src/App/Pages/Vault/CiphersPage.xaml b/src/App/Pages/Vault/CiphersPage.xaml new file mode 100644 index 000000000..d8ce22500 --- /dev/null +++ b/src/App/Pages/Vault/CiphersPage.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/CiphersPage.xaml.cs b/src/App/Pages/Vault/CiphersPage.xaml.cs new file mode 100644 index 000000000..55bf634e1 --- /dev/null +++ b/src/App/Pages/Vault/CiphersPage.xaml.cs @@ -0,0 +1,122 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class CiphersPage : BaseContentPage + { + private readonly string _autofillUrl; + private readonly IDeviceActionService _deviceActionService; + + private CiphersPageViewModel _vm; + private bool _hasFocused; + + public CiphersPage(Func filter, bool folder = false, bool collection = false, + bool type = false, string autofillUrl = null) + { + InitializeComponent(); + _vm = BindingContext as CiphersPageViewModel; + _vm.Page = this; + _vm.Filter = filter; + _vm.AutofillUrl = _autofillUrl = autofillUrl; + if(folder) + { + _vm.PageTitle = AppResources.SearchFolder; + } + else if(collection) + { + _vm.PageTitle = AppResources.SearchCollection; + } + else if(type) + { + _vm.PageTitle = AppResources.SearchType; + } + else + { + _vm.PageTitle = AppResources.SearchVault; + } + + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + } + + public SearchBar SearchBar => _searchBar; + + protected override void OnAppearing() + { + base.OnAppearing(); + if(!_hasFocused) + { + _hasFocused = true; + if(string.IsNullOrWhiteSpace(_autofillUrl)) + { + RequestFocus(_searchBar); + } + } + } + + private void SearchBar_TextChanged(object sender, TextChangedEventArgs e) + { + var oldLength = e.OldTextValue?.Length ?? 0; + var newLength = e.NewTextValue?.Length ?? 0; + if(oldLength < 2 && newLength < 2 && oldLength < newLength) + { + return; + } + _vm.Search(e.NewTextValue, 300); + } + + private void SearchBar_SearchButtonPressed(object sender, EventArgs e) + { + _vm.Search((sender as SearchBar).Text); + } + + private void BackButton_Clicked(object sender, EventArgs e) + { + GoBack(); + } + + protected override bool OnBackButtonPressed() + { + if(string.IsNullOrWhiteSpace(_autofillUrl)) + { + return false; + } + GoBack(); + return true; + } + + private void GoBack() + { + if(!DoOnce()) + { + return; + } + if(string.IsNullOrWhiteSpace(_autofillUrl)) + { + Navigation.PopModalAsync(false); + } + else + { + _deviceActionService.CloseAutofill(); + } + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(!DoOnce()) + { + return; + } + + if(e.SelectedItem is CipherView cipher) + { + await _vm.SelectCipherAsync(cipher); + } + } + } +} diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs new file mode 100644 index 000000000..8d1972054 --- /dev/null +++ b/src/App/Pages/Vault/CiphersPageViewModel.cs @@ -0,0 +1,180 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class CiphersPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly ICipherService _cipherService; + private readonly ISearchService _searchService; + private readonly IDeviceActionService _deviceActionService; + private CancellationTokenSource _searchCancellationTokenSource; + + private string _searchText; + private bool _showNoData; + private bool _showList; + + public CiphersPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _searchService = ServiceContainer.Resolve("searchService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + + Ciphers = new ExtendedObservableCollection(); + CipherOptionsCommand = new Command(CipherOptionsAsync); + } + + public Command CipherOptionsCommand { get; set; } + public ExtendedObservableCollection Ciphers { get; set; } + public Func Filter { get; set; } + public string AutofillUrl { get; set; } + + public string SearchText + { + get => _searchText; + set => SetProperty(ref _searchText, value); + } + + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } + + public bool ShowList + { + get => _showList; + set => SetProperty(ref _showList, value); + } + + public void Search(string searchText, int? timeout = null) + { + var previousCts = _searchCancellationTokenSource; + var cts = new CancellationTokenSource(); + Task.Run(async () => + { + List ciphers = null; + var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1; + if(searchable) + { + if(timeout != null) + { + await Task.Delay(timeout.Value); + } + if(searchText != (Page as CiphersPage).SearchBar.Text) + { + return; + } + else + { + previousCts?.Cancel(); + } + try + { + ciphers = await _searchService.SearchCiphersAsync(searchText, Filter, null, cts.Token); + cts.Token.ThrowIfCancellationRequested(); + Ciphers.ResetWithRange(ciphers); + ShowNoData = Ciphers.Count == 0; + } + catch(OperationCanceledException) + { + ciphers = new List(); + } + } + if(ciphers == null) + { + ciphers = new List(); + } + Ciphers.ResetWithRange(ciphers); + ShowNoData = searchable && Ciphers.Count == 0; + ShowList = searchable && !ShowNoData; + }, cts.Token); + _searchCancellationTokenSource = cts; + } + + public async Task SelectCipherAsync(CipherView cipher) + { + string selection = null; + if(!string.IsNullOrWhiteSpace(AutofillUrl)) + { + var options = new List { AppResources.Autofill }; + if(cipher.Type == CipherType.Login) + { + options.Add(AppResources.AutofillAndSave); + } + options.Add(AppResources.View); + selection = await Page.DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null, + options.ToArray()); + } + if(selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl)) + { + var page = new ViewPage(cipher.Id); + await Page.Navigation.PushModalAsync(new NavigationPage(page)); + } + else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) + { + if(selection == AppResources.AutofillAndSave) + { + var uris = cipher.Login?.Uris?.ToList(); + if(uris == null) + { + uris = new List(); + } + uris.Add(new LoginUriView + { + Uri = AutofillUrl, + Match = null + }); + cipher.Login.Uris = uris; + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher)); + await _deviceActionService.HideLoadingAsync(); + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), + AppResources.Ok); + } + } + if(_deviceActionService.SystemMajorVersion() < 21) + { + // TODO + } + else + { + _deviceActionService.Autofill(cipher); + } + } + } + + private async void CipherOptionsAsync(CipherView cipher) + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + var option = await Page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, "1", "2"); + if(option == AppResources.Cancel) + { + return; + } + // TODO: process options + } + } +} diff --git a/src/App/Pages/Vault/CollectionsPage.xaml b/src/App/Pages/Vault/CollectionsPage.xaml new file mode 100644 index 000000000..6f8031af0 --- /dev/null +++ b/src/App/Pages/Vault/CollectionsPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/CollectionsPage.xaml.cs b/src/App/Pages/Vault/CollectionsPage.xaml.cs new file mode 100644 index 000000000..13d08385f --- /dev/null +++ b/src/App/Pages/Vault/CollectionsPage.xaml.cs @@ -0,0 +1,37 @@ +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class CollectionsPage : BaseContentPage + { + private CollectionsPageViewModel _vm; + + public CollectionsPage(string cipherId) + { + InitializeComponent(); + _vm = BindingContext as CollectionsPageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + SetActivityIndicator(); + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync()); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + } + + private async void Save_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + } +} diff --git a/src/App/Pages/Vault/CollectionsPageViewModel.cs b/src/App/Pages/Vault/CollectionsPageViewModel.cs new file mode 100644 index 000000000..b854a6402 --- /dev/null +++ b/src/App/Pages/Vault/CollectionsPageViewModel.cs @@ -0,0 +1,87 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class CollectionsPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + private readonly ICollectionService _collectionService; + private readonly IPlatformUtilsService _platformUtilsService; + private CipherView _cipher; + private Cipher _cipherDomain; + private bool _hasCollections; + + public CollectionsPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _collectionService = ServiceContainer.Resolve("collectionService"); + Collections = new ExtendedObservableCollection(); + PageTitle = AppResources.Collections; + } + + public string CipherId { get; set; } + public ExtendedObservableCollection Collections { get; set; } + public bool HasCollections + { + get => _hasCollections; + set => SetProperty(ref _hasCollections, value); + } + + public async Task LoadAsync() + { + _cipherDomain = await _cipherService.GetAsync(CipherId); + var collectionIds = _cipherDomain.CollectionIds; + _cipher = await _cipherDomain.DecryptAsync(); + var allCollections = await _collectionService.GetAllDecryptedAsync(); + var collections = allCollections + .Where(c => !c.ReadOnly && c.OrganizationId == _cipher.OrganizationId) + .Select(c => new CollectionViewModel + { + Collection = c, + Checked = collectionIds.Contains(c.Id) + }).ToList(); + Collections.ResetWithRange(collections); + HasCollections = Collections.Any(); + } + + public async Task SubmitAsync() + { + if(!Collections.Any(c => c.Checked)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, + AppResources.Ok); + return false; + } + + _cipherDomain.CollectionIds = new HashSet( + Collections.Where(c => c.Checked).Select(c => c.Collection.Id)); + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.SaveCollectionsWithServerAsync(_cipherDomain); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.ItemUpdated); + await Page.Navigation.PopModalAsync(); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml new file mode 100644 index 000000000..202ff8cf5 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RecycleElement + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs new file mode 100644 index 000000000..9053f02f6 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -0,0 +1,158 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class GroupingsPage : BaseContentPage + { + private readonly IBroadcasterService _broadcasterService; + private readonly ISyncService _syncService; + private readonly IPushNotificationService _pushNotificationService; + private readonly IStorageService _storageService; + private readonly GroupingsPageViewModel _vm; + private readonly string _pageName; + + public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null, + string collectionId = null, string pageTitle = null) + { + _pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks); + InitializeComponent(); + SetActivityIndicator(_mainContent); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _syncService = ServiceContainer.Resolve("syncService"); + _pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); + _storageService = ServiceContainer.Resolve("storageService"); + _vm = BindingContext as GroupingsPageViewModel; + _vm.Page = this; + _vm.MainPage = mainPage; + _vm.Type = type; + _vm.FolderId = folderId; + _vm.CollectionId = collectionId; + if(pageTitle != null) + { + _vm.PageTitle = pageTitle; + } + + if(Device.RuntimePlatform == Device.iOS) + { + _absLayout.Children.Remove(_fab); + } + else + { + _fab.Clicked = AddButton_Clicked; + } + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + _broadcasterService.Subscribe(_pageName, async (message) => + { + if(message.Command == "syncCompleted") + { + await Task.Delay(500); + // await _viewModel.LoadAsync(); + } + }); + + await LoadOnAppearedAsync(_mainLayout, false, async () => + { + if(!_syncService.SyncInProgress) + { + await _vm.LoadAsync(); + } + else + { + await Task.Delay(5000); + if(!_vm.Loaded) + { + await _vm.LoadAsync(); + } + } + }, _mainContent); + + // Push registration + var lastPushRegistration = await _storageService.GetAsync(Constants.PushLastRegistrationDateKey); + lastPushRegistration = lastPushRegistration.GetValueOrDefault(DateTime.MinValue); + if(Device.RuntimePlatform == Device.iOS) + { + var pushPromptShow = await _storageService.GetAsync(Constants.PushInitialPromptShownKey); + if(!pushPromptShow.GetValueOrDefault(false)) + { + await _storageService.SaveAsync(Constants.PushInitialPromptShownKey, true); + await DisplayAlert(AppResources.EnableAutomaticSyncing, AppResources.PushNotificationAlert, + AppResources.OkGotIt); + } + if(!pushPromptShow.GetValueOrDefault(false) || + DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1)) + { + await _pushNotificationService.RegisterAsync(); + } + } + else if(Device.RuntimePlatform == Device.Android && + DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1)) + { + await _pushNotificationService.RegisterAsync(); + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _broadcasterService.Unsubscribe(_pageName); + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(!DoOnce()) + { + return; + } + if(!(e.SelectedItem is GroupingsPageListItem item)) + { + return; + } + + if(item.Cipher != null) + { + await _vm.SelectCipherAsync(item.Cipher); + } + else if(item.Folder != null) + { + await _vm.SelectFolderAsync(item.Folder); + } + else if(item.Collection != null) + { + await _vm.SelectCollectionAsync(item.Collection); + } + else if(item.Type != null) + { + await _vm.SelectTypeAsync(item.Type.Value); + } + } + + private async void Search_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null, + _vm.Type != null); + await Navigation.PushModalAsync(new NavigationPage(page), false); + } + } + + private async void AddButton_Clicked(object sender, System.EventArgs e) + { + var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListGroup.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListGroup.cs new file mode 100644 index 000000000..b216b6d0a --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListGroup.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Bit.App.Pages +{ + public class GroupingsPageListGroup : List + { + public GroupingsPageListGroup(string name, int count, bool doUpper = true) + : this(new List(), name, count, doUpper) + { } + + public GroupingsPageListGroup(List groupItems, string name, int count, + bool doUpper = true) + { + AddRange(groupItems); + if(string.IsNullOrWhiteSpace(name)) + { + Name = "-"; + } + else if(doUpper) + { + Name = name.ToUpperInvariant(); + } + else + { + Name = name; + } + ItemCount = count.ToString("N0"); + } + + public string Name { get; set; } + public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString(); + public string ItemCount { get; set; } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs new file mode 100644 index 000000000..6dfd33b78 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs @@ -0,0 +1,100 @@ +using Bit.App.Resources; +using Bit.Core.Enums; +using Bit.Core.Models.View; + +namespace Bit.App.Pages +{ + public class GroupingsPageListItem + { + private string _icon; + private string _name; + + public FolderView Folder { get; set; } + public CollectionView Collection { get; set; } + public CipherView Cipher { get; set; } + public CipherType? Type { get; set; } + public string ItemCount { get; set; } + public bool FuzzyAutofill { get; set; } + + public string Name + { + get + { + if(_name != null) + { + return _name; + } + if(Folder != null) + { + _name = Folder.Name; + } + else if(Collection != null) + { + _name = Collection.Name; + } + else if(Type != null) + { + switch(Type.Value) + { + case CipherType.Login: + _name = AppResources.TypeLogin; + break; + case CipherType.SecureNote: + _name = AppResources.TypeSecureNote; + break; + case CipherType.Card: + _name = AppResources.TypeCard; + break; + case CipherType.Identity: + _name = AppResources.TypeIdentity; + break; + default: + break; + } + } + return _name; + } + } + + public string Icon + { + get + { + if(_icon != null) + { + return _icon; + } + if(Folder != null) + { + _icon = Folder.Id == null ? "" : ""; + } + else if(Collection != null) + { + _icon = ""; + } + else if(Type != null) + { + switch(Type.Value) + { + case CipherType.Login: + _icon = ""; + break; + case CipherType.SecureNote: + _icon = ""; + break; + case CipherType.Card: + _icon = ""; + break; + case CipherType.Identity: + _icon = ""; + break; + default: + _icon = ""; + break; + } + } + return _icon; + } + } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs new file mode 100644 index 000000000..4fea472ba --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs @@ -0,0 +1,19 @@ +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class GroupingsPageListItemSelector : DataTemplateSelector + { + public DataTemplate CipherTemplate { get; set; } + public DataTemplate GroupTemplate { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + if(item is GroupingsPageListItem listItem) + { + return listItem.Cipher != null ? CipherTemplate : GroupTemplate; + } + return null; + } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs new file mode 100644 index 000000000..9eb810017 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -0,0 +1,382 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class GroupingsPageViewModel : BaseViewModel + { + private bool _refreshing; + private bool _loading; + private bool _loaded; + private bool _showAddCipherButton; + private bool _showNoData; + private bool _showList; + private string _noDataText; + private List _allCiphers; + private Dictionary _folderCounts = new Dictionary(); + private Dictionary _collectionCounts = new Dictionary(); + private Dictionary _typeCounts = new Dictionary(); + + private readonly ICipherService _cipherService; + private readonly IFolderService _folderService; + private readonly ICollectionService _collectionService; + private readonly ISyncService _syncService; + + public GroupingsPageViewModel() + { + _cipherService = ServiceContainer.Resolve("cipherService"); + _folderService = ServiceContainer.Resolve("folderService"); + _collectionService = ServiceContainer.Resolve("collectionService"); + _syncService = ServiceContainer.Resolve("syncService"); + + Loading = true; + PageTitle = AppResources.MyVault; + GroupedItems = new ExtendedObservableCollection(); + RefreshCommand = new Command(async () => + { + Refreshing = true; + await LoadAsync(); + }); + CipherOptionsCommand = new Command(CipherOptionsAsync); + } + + public bool ShowFavorites { get; set; } = true; + public bool ShowFolders { get; set; } = true; + public bool ShowCollections { get; set; } = true; + public bool MainPage { get; set; } + public CipherType? Type { get; set; } + public string FolderId { get; set; } + public string CollectionId { get; set; } + public Func Filter { get; set; } + + public List Ciphers { get; set; } + public List FavoriteCiphers { get; set; } + public List NoFolderCiphers { get; set; } + public List Folders { get; set; } + public List> NestedFolders { get; set; } + public List Collections { get; set; } + public List> NestedCollections { get; set; } + + public bool Refreshing + { + get => _refreshing; + set => SetProperty(ref _refreshing, value); + } + public bool Loading + { + get => _loading; + set => SetProperty(ref _loading, value); + } + public bool Loaded + { + get => _loaded; + set => SetProperty(ref _loaded, value); + } + public bool ShowAddCipherButton + { + get => _showAddCipherButton; + set => SetProperty(ref _showAddCipherButton, value); + } + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } + public string NoDataText + { + get => _noDataText; + set => SetProperty(ref _noDataText, value); + } + public bool ShowList + { + get => _showList; + set => SetProperty(ref _showList, value); + } + public ExtendedObservableCollection GroupedItems { get; set; } + public Command RefreshCommand { get; set; } + public Command CipherOptionsCommand { get; set; } + + public async Task LoadAsync() + { + ShowNoData = false; + Loading = true; + ShowList = false; + ShowAddCipherButton = true; + var groupedItems = new List(); + + try + { + await LoadDataAsync(); + + var favListItems = FavoriteCiphers?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); + var ciphersListItems = Ciphers?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); + var folderListItems = NestedFolders?.Select(f => + { + var fId = f.Node.Id ?? "none"; + return new GroupingsPageListItem + { + Folder = f.Node, + ItemCount = (_folderCounts.ContainsKey(fId) ? _folderCounts[fId] : 0).ToString("N0") + }; + }).ToList(); + var collectionListItems = NestedCollections?.Select(c => new GroupingsPageListItem + { + Collection = c.Node, + ItemCount = (_collectionCounts.ContainsKey(c.Node.Id) ? + _collectionCounts[c.Node.Id] : 0).ToString("N0") + }).ToList(); + + if(favListItems?.Any() ?? false) + { + groupedItems.Add(new GroupingsPageListGroup(favListItems, AppResources.Favorites, + favListItems.Count, Device.RuntimePlatform == Device.iOS)); + } + if(MainPage) + { + groupedItems.Add(new GroupingsPageListGroup( + AppResources.Types, 4, Device.RuntimePlatform == Device.iOS) + { + new GroupingsPageListItem + { + Type = CipherType.Login, + ItemCount = (_typeCounts.ContainsKey(CipherType.Login) ? + _typeCounts[CipherType.Login] : 0).ToString("N0") + }, + new GroupingsPageListItem + { + Type = CipherType.Card, + ItemCount = (_typeCounts.ContainsKey(CipherType.Card) ? + _typeCounts[CipherType.Card] : 0).ToString("N0") + }, + new GroupingsPageListItem + { + Type = CipherType.Identity, + ItemCount = (_typeCounts.ContainsKey(CipherType.Identity) ? + _typeCounts[CipherType.Identity] : 0).ToString("N0") + }, + new GroupingsPageListItem + { + Type = CipherType.SecureNote, + ItemCount = (_typeCounts.ContainsKey(CipherType.SecureNote) ? + _typeCounts[CipherType.SecureNote] : 0).ToString("N0") + }, + }); + } + if(folderListItems?.Any() ?? false) + { + groupedItems.Add(new GroupingsPageListGroup(folderListItems, AppResources.Folders, + folderListItems.Count, Device.RuntimePlatform == Device.iOS)); + } + if(collectionListItems?.Any() ?? false) + { + groupedItems.Add(new GroupingsPageListGroup(collectionListItems, AppResources.Collections, + collectionListItems.Count, Device.RuntimePlatform == Device.iOS)); + } + if(ciphersListItems?.Any() ?? false) + { + groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, + ciphersListItems.Count, Device.RuntimePlatform == Device.iOS)); + } + GroupedItems.ResetWithRange(groupedItems); + } + finally + { + ShowNoData = !groupedItems.Any(); + ShowList = !ShowNoData; + Loaded = true; + Loading = false; + Refreshing = false; + } + } + + public async Task SelectCipherAsync(CipherView cipher) + { + var page = new ViewPage(cipher.Id); + await Page.Navigation.PushModalAsync(new NavigationPage(page)); + } + + public async Task SelectTypeAsync(CipherType type) + { + string title = null; + switch(type) + { + case CipherType.Login: + title = AppResources.Logins; + break; + case CipherType.SecureNote: + title = AppResources.SecureNotes; + break; + case CipherType.Card: + title = AppResources.Cards; + break; + case CipherType.Identity: + title = AppResources.Identities; + break; + default: + break; + } + var page = new GroupingsPage(false, type, null, null, title); + await Page.Navigation.PushAsync(page); + } + + public async Task SelectFolderAsync(FolderView folder) + { + var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name); + await Page.Navigation.PushAsync(page); + } + + public async Task SelectCollectionAsync(Core.Models.View.CollectionView collection) + { + var page = new GroupingsPage(false, null, null, collection.Id, collection.Name); + await Page.Navigation.PushAsync(page); + } + + private async Task LoadDataAsync() + { + NoDataText = AppResources.NoItems; + _allCiphers = await _cipherService.GetAllDecryptedAsync(); + FavoriteCiphers?.Clear(); + NoFolderCiphers?.Clear(); + _folderCounts.Clear(); + _collectionCounts.Clear(); + _typeCounts.Clear(); + Filter = null; + + if(MainPage) + { + if(ShowFolders) + { + Folders = await _folderService.GetAllDecryptedAsync(); + NestedFolders = await _folderService.GetAllNestedAsync(); + } + if(ShowCollections) + { + Collections = await _collectionService.GetAllDecryptedAsync(); + NestedCollections = await _collectionService.GetAllNestedAsync(Collections); + } + } + else + { + if(Type != null) + { + Filter = c => c.Type == Type.Value; + } + else if(FolderId != null) + { + NoDataText = AppResources.NoItemsFolder; + var folderId = FolderId == "none" ? null : FolderId; + if(folderId != null) + { + var folderNode = await _folderService.GetNestedAsync(folderId); + if(folderNode?.Node != null) + { + PageTitle = folderNode.Node.Name; + NestedFolders = (folderNode.Children?.Count ?? 0) > 0 ? folderNode.Children : null; + } + } + else + { + PageTitle = AppResources.FolderNone; + } + Filter = c => c.FolderId == folderId; + } + else if(CollectionId != null) + { + ShowAddCipherButton = false; + NoDataText = AppResources.NoItemsCollection; + var collectionNode = await _collectionService.GetNestedAsync(CollectionId); + if(collectionNode?.Node != null) + { + PageTitle = collectionNode.Node.Name; + } + Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false; + } + else + { + PageTitle = AppResources.AllItems; + } + Ciphers = Filter != null ? _allCiphers.Where(Filter).ToList() : _allCiphers; + } + + foreach(var c in _allCiphers) + { + if(MainPage) + { + if(c.Favorite) + { + if(FavoriteCiphers == null) + { + FavoriteCiphers = new List(); + } + FavoriteCiphers.Add(c); + } + if(c.FolderId == null) + { + if(NoFolderCiphers == null) + { + NoFolderCiphers = new List(); + } + NoFolderCiphers.Add(c); + } + + if(_typeCounts.ContainsKey(c.Type)) + { + _typeCounts[c.Type] = _typeCounts[c.Type] + 1; + } + else + { + _typeCounts.Add(c.Type, 1); + } + } + + var fId = c.FolderId ?? "none"; + if(_folderCounts.ContainsKey(fId)) + { + _folderCounts[fId] = _folderCounts[fId] + 1; + } + else + { + _folderCounts.Add(fId, 1); + } + + if(c.CollectionIds != null) + { + foreach(var colId in c.CollectionIds) + { + if(_collectionCounts.ContainsKey(colId)) + { + _collectionCounts[colId] = _collectionCounts[colId] + 1; + } + else + { + _collectionCounts.Add(colId, 1); + } + } + } + } + } + + private async void CipherOptionsAsync(CipherView cipher) + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + var option = await Page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, "1", "2"); + if(option == AppResources.Cancel) + { + return; + } + // TODO: process options + } + } +} diff --git a/src/App/Pages/Vault/PasswordHistoryPage.xaml b/src/App/Pages/Vault/PasswordHistoryPage.xaml new file mode 100644 index 000000000..4adc60f61 --- /dev/null +++ b/src/App/Pages/Vault/PasswordHistoryPage.xaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs b/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs new file mode 100644 index 000000000..48fb9a570 --- /dev/null +++ b/src/App/Pages/Vault/PasswordHistoryPage.xaml.cs @@ -0,0 +1,26 @@ +using System; + +namespace Bit.App.Pages +{ + public partial class PasswordHistoryPage : BaseContentPage + { + private PasswordHistoryPageViewModel _vm; + + public PasswordHistoryPage(string cipherId) + { + InitializeComponent(); + SetActivityIndicator(); + _vm = BindingContext as PasswordHistoryPageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, true, async () => { + await _vm.InitAsync(); + }); + } + } +} diff --git a/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs new file mode 100644 index 000000000..49a25495e --- /dev/null +++ b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs @@ -0,0 +1,53 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class PasswordHistoryPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly ICipherService _cipherService; + + private bool _showNoData; + + public PasswordHistoryPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + + PageTitle = AppResources.PasswordHistory; + History = new ExtendedObservableCollection(); + CopyCommand = new Command(CopyAsync); + } + + public Command CopyCommand { get; set; } + public string CipherId { get; set; } + public ExtendedObservableCollection History { get; set; } + + public bool ShowNoData + { + get => _showNoData; + set => SetProperty(ref _showNoData, value); + } + + public async Task InitAsync() + { + var cipher = await _cipherService.GetAsync(CipherId); + var decCipher = await cipher.DecryptAsync(); + History.ResetWithRange(decCipher.PasswordHistory ?? new List()); + ShowNoData = History.Count == 0; + } + + private async void CopyAsync(PasswordHistoryView ph) + { + await _platformUtilsService.CopyToClipboardAsync(ph.Password); + _platformUtilsService.ShowToast("info", null, + string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); + } + } +} diff --git a/src/App/Pages/Vault/ScanPage.xaml b/src/App/Pages/Vault/ScanPage.xaml new file mode 100644 index 000000000..b6b6fff8a --- /dev/null +++ b/src/App/Pages/Vault/ScanPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/ScanPage.xaml.cs b/src/App/Pages/Vault/ScanPage.xaml.cs new file mode 100644 index 000000000..8d61854bd --- /dev/null +++ b/src/App/Pages/Vault/ScanPage.xaml.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class ScanPage : BaseContentPage + { + private readonly Action _callback; + + private DateTime? _timerStarted = null; + private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(3); + + public ScanPage(Action callback) + { + _callback = callback; + InitializeComponent(); + _zxing.Options = new ZXing.Mobile.MobileBarcodeScanningOptions + { + UseNativeScanning = true, + PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, + AutoRotate = false, + }; + } + + protected override void OnAppearing() + { + base.OnAppearing(); + _zxing.IsScanning = true; + _timerStarted = DateTime.Now; + Device.StartTimer(new TimeSpan(0, 0, 2), () => + { + if(_timerStarted == null || (DateTime.Now - _timerStarted) > _timerMaxLength) + { + return false; + } + _zxing.AutoFocus(); + return true; + }); + } + + protected override void OnDisappearing() + { + _timerStarted = null; + _zxing.IsScanning = false; + base.OnDisappearing(); + } + + private void OnScanResult(ZXing.Result result) + { + // Stop analysis until we navigate away so we don't keep reading barcodes + _zxing.IsAnalyzing = false; + _zxing.IsScanning = false; + if(!string.IsNullOrWhiteSpace(result?.Text) && + Uri.TryCreate(result.Text, UriKind.Absolute, out Uri uri) && + !string.IsNullOrWhiteSpace(uri?.Query)) + { + var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&'); + foreach(var part in queryParts) + { + if(part.StartsWith("secret=")) + { + _callback(part.Substring(7)?.ToUpperInvariant()); + return; + } + } + } + _callback(null); + } + } +} diff --git a/src/App/Pages/Vault/SharePage.xaml b/src/App/Pages/Vault/SharePage.xaml new file mode 100644 index 000000000..e276d85f8 --- /dev/null +++ b/src/App/Pages/Vault/SharePage.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/SharePage.xaml.cs b/src/App/Pages/Vault/SharePage.xaml.cs new file mode 100644 index 000000000..37b4be847 --- /dev/null +++ b/src/App/Pages/Vault/SharePage.xaml.cs @@ -0,0 +1,38 @@ +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class SharePage : BaseContentPage + { + private SharePageViewModel _vm; + + public SharePage(string cipherId) + { + InitializeComponent(); + _vm = BindingContext as SharePageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + SetActivityIndicator(); + _organizationPicker.ItemDisplayBinding = new Binding("Key"); + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync()); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + } + + private async void Save_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + } +} diff --git a/src/App/Pages/Vault/SharePageViewModel.cs b/src/App/Pages/Vault/SharePageViewModel.cs new file mode 100644 index 000000000..9af5eaa4f --- /dev/null +++ b/src/App/Pages/Vault/SharePageViewModel.cs @@ -0,0 +1,147 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class SharePageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + private readonly ICollectionService _collectionService; + private readonly IUserService _userService; + private readonly IPlatformUtilsService _platformUtilsService; + private CipherView _cipher; + private int _organizationSelectedIndex; + private bool _hasCollections; + private bool _hasOrganizations; + private List _writeableCollections; + + public SharePageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _userService = ServiceContainer.Resolve("userService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _collectionService = ServiceContainer.Resolve("collectionService"); + Collections = new ExtendedObservableCollection(); + OrganizationOptions = new List>(); + PageTitle = AppResources.Share; + } + + public string CipherId { get; set; } + public string OrganizationId { get; set; } + public List> OrganizationOptions { get; set; } + public ExtendedObservableCollection Collections { get; set; } + public int OrganizationSelectedIndex + { + get => _organizationSelectedIndex; + set + { + if(SetProperty(ref _organizationSelectedIndex, value)) + { + OrganizationChanged(); + } + } + } + public bool HasCollections + { + get => _hasCollections; + set => SetProperty(ref _hasCollections, value); + } + public bool HasOrganizations + { + get => _hasOrganizations; + set => SetProperty(ref _hasOrganizations, value); + } + + public async Task LoadAsync() + { + var allCollections = await _collectionService.GetAllDecryptedAsync(); + _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); + + var orgs = await _userService.GetAllOrganizationAsync(); + OrganizationOptions = orgs.OrderBy(o => o.Name) + .Where(o => o.Enabled && o.Status == OrganizationUserStatusType.Confirmed) + .Select(o => new KeyValuePair(o.Name, o.Id)).ToList(); + HasOrganizations = OrganizationOptions.Any(); + + var cipherDomain = await _cipherService.GetAsync(CipherId); + _cipher = await cipherDomain.DecryptAsync(); + if(OrganizationId == null && OrganizationOptions.Any()) + { + OrganizationId = OrganizationOptions.First().Value; + } + OrganizationSelectedIndex = string.IsNullOrWhiteSpace(OrganizationId) ? 0 : + OrganizationOptions.FindIndex(k => k.Value == OrganizationId); + FilterCollections(); + } + + public async Task SubmitAsync() + { + if(!Collections?.Any(c => c.Checked) ?? true) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, + AppResources.Ok); + return false; + } + + var cipherDomain = await _cipherService.GetAsync(CipherId); + var cipherView = await cipherDomain.DecryptAsync(); + + var checkedCollectionIds = new HashSet( + Collections.Where(c => c.Checked).Select(c => c.Collection.Id)); + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.ItemShared); + await Page.Navigation.PopModalAsync(); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + catch(System.Exception e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Message, AppResources.Ok); + } + return false; + } + + private void OrganizationChanged() + { + if(OrganizationSelectedIndex > -1) + { + OrganizationId = OrganizationOptions[OrganizationSelectedIndex].Value; + FilterCollections(); + } + } + + private void FilterCollections() + { + if(OrganizationId == null || !_writeableCollections.Any()) + { + Collections.ResetWithRange(new List()); + } + else + { + var cols = _writeableCollections.Where(c => c.OrganizationId == OrganizationId) + .Select(c => new CollectionViewModel { Collection = c }).ToList(); + Collections.ResetWithRange(cols); + } + HasCollections = Collections.Any(); + } + } +} diff --git a/src/App/Pages/Vault/VaultAddCipherPage.cs b/src/App/Pages/Vault/VaultAddCipherPage.cs deleted file mode 100644 index 7ceac3b3e..000000000 --- a/src/App/Pages/Vault/VaultAddCipherPage.cs +++ /dev/null @@ -1,855 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models; -using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using Plugin.Settings.Abstractions; -using Bit.App.Utilities; -using Bit.App.Enums; - -namespace Bit.App.Pages -{ - public class VaultAddCipherPage : ExtendedContentPage - { - private const string AddedLoginAlertKey = "addedSiteAlert"; - - private readonly CipherType _type; - private readonly ICipherService _cipherService; - private readonly IFolderService _folderService; - private readonly IConnectivity _connectivity; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly ISettings _settings; - private readonly IAppInfoService _appInfoService; - private readonly IDeviceInfoService _deviceInfo; - private readonly IDeviceActionService _deviceActionService; - private readonly string _defaultFolderId; - private readonly string _defaultUri; - private readonly string _defaultName; - private readonly string _defaultUsername; - private readonly string _defaultPassword; - private readonly string _defaultCardName; - private readonly string _defaultCardNumber; - private readonly int? _defaultCardExpMonth; - private readonly string _defaultCardExpYear; - private readonly string _defaultCardCode; - private readonly bool _fromAutofill; - private readonly bool _fromAutofillFramework; - private DateTime? _lastAction; - - public VaultAddCipherPage(AppOptions options) - : this(options.SaveType.Value, options.Uri, options.SaveName, options.FromAutofillFramework, false) - { - _fromAutofillFramework = options.FromAutofillFramework; - _defaultUsername = options.SaveUsername; - _defaultPassword = options.SavePassword; - _defaultCardCode = options.SaveCardCode; - if(int.TryParse(options.SaveCardExpMonth, out int month) && month <= 12 && month >= 1) - { - _defaultCardExpMonth = month; - } - _defaultCardExpYear = options.SaveCardExpYear; - _defaultCardName = options.SaveCardName; - _defaultCardNumber = options.SaveCardNumber; - Init(); - } - - public VaultAddCipherPage(CipherType type, string defaultUri = null, string defaultName = null, - bool fromAutofill = false, bool doInit = true, string defaultFolderId = null) - { - _defaultFolderId = defaultFolderId; - _type = type; - _defaultUri = defaultUri; - _defaultName = defaultName; - _fromAutofill = fromAutofill; - - _cipherService = Resolver.Resolve(); - _folderService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _appInfoService = Resolver.Resolve(); - _deviceInfo = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - - if(doInit) - { - Init(); - } - } - - public List Folders { get; set; } - public TableRoot TableRoot { get; set; } - public TableSection TopSection { get; set; } - public TableSection UrisSection { get; set; } - public TableSection MiddleSection { get; set; } - public TableSection FieldsSection { get; set; } - public ExtendedTableView Table { get; set; } - - public FormEntryCell NameCell { get; private set; } - public FormEditorCell NotesCell { get; private set; } - public FormPickerCell FolderCell { get; private set; } - public ExtendedSwitchCell FavoriteCell { get; set; } - public ExtendedTextCell AddFieldCell { get; private set; } - public ExtendedTextCell AddUriCell { get; private set; } - - // Login - public FormEntryCell LoginPasswordCell { get; private set; } - public FormEntryCell LoginUsernameCell { get; private set; } - public FormEntryCell LoginTotpCell { get; private set; } - - // Card - public FormEntryCell CardNameCell { get; private set; } - public FormEntryCell CardNumberCell { get; private set; } - public FormPickerCell CardBrandCell { get; private set; } - public FormPickerCell CardExpMonthCell { get; private set; } - public FormEntryCell CardExpYearCell { get; private set; } - public FormEntryCell CardCodeCell { get; private set; } - - // Identity - public FormPickerCell IdTitleCell { get; private set; } - public FormEntryCell IdFirstNameCell { get; private set; } - public FormEntryCell IdMiddleNameCell { get; private set; } - public FormEntryCell IdLastNameCell { get; private set; } - public FormEntryCell IdUsernameCell { get; private set; } - public FormEntryCell IdCompanyCell { get; private set; } - public FormEntryCell IdSsnCell { get; private set; } - public FormEntryCell IdPassportNumberCell { get; private set; } - public FormEntryCell IdLicenseNumberCell { get; private set; } - public FormEntryCell IdEmailCell { get; private set; } - public FormEntryCell IdPhoneCell { get; private set; } - public FormEntryCell IdAddress1Cell { get; private set; } - public FormEntryCell IdAddress2Cell { get; private set; } - public FormEntryCell IdAddress3Cell { get; private set; } - public FormEntryCell IdCityCell { get; private set; } - public FormEntryCell IdStateCell { get; private set; } - public FormEntryCell IdPostalCodeCell { get; private set; } - public FormEntryCell IdCountryCell { get; private set; } - - private void Init() - { - // Name - NameCell = new FormEntryCell(AppResources.Name); - if(!string.IsNullOrWhiteSpace(_defaultName)) - { - NameCell.Entry.Text = _defaultName; - } - - // Notes - NotesCell = new FormEditorCell(Keyboard.Text, _type == CipherType.SecureNote ? 500 : 180); - - // Folders - var folderOptions = new List { AppResources.FolderNone }; - Folders = _folderService.GetAllAsync().GetAwaiter().GetResult() - .OrderBy(f => f.Name?.Decrypt()).ToList(); - var selectedIndex = 0; - var i = 1; - foreach(var folder in Folders) - { - if(folder.Id == _defaultFolderId) - { - selectedIndex = i; - } - folderOptions.Add(folder.Name.Decrypt()); - i++; - } - FolderCell = new FormPickerCell(AppResources.Folder, folderOptions.ToArray()); - FolderCell.Picker.SelectedIndex = selectedIndex; - - // Favorite - FavoriteCell = new ExtendedSwitchCell { Text = AppResources.Favorite }; - - InitTable(); - InitSave(); - - Title = AppResources.AddItem; - Content = Table; - if(Device.RuntimePlatform == Device.iOS) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - } - - protected override void OnAppearing() - { - base.OnAppearing(); - - NameCell.InitEvents(); - NotesCell.InitEvents(); - FolderCell.InitEvents(); - - if(AddFieldCell != null) - { - AddFieldCell.Tapped += AddFieldCell_Tapped; - } - if(AddUriCell != null) - { - AddUriCell.Tapped += AddUriCell_Tapped; - } - - switch(_type) - { - case CipherType.Login: - LoginPasswordCell.InitEvents(); - LoginUsernameCell.InitEvents(); - LoginTotpCell.InitEvents(); - LoginPasswordCell.Button1.Clicked += PasswordButton_Clicked; - LoginPasswordCell.Button2.Clicked += PasswordButton2_Clicked; - if(LoginTotpCell?.Button1 != null) - { - LoginTotpCell.Button1.Clicked += TotpButton_Clicked; - } - break; - case CipherType.Card: - CardBrandCell.InitEvents(); - CardCodeCell.InitEvents(); - CardExpMonthCell.InitEvents(); - CardExpYearCell.InitEvents(); - CardNameCell.InitEvents(); - CardNumberCell.InitEvents(); - CardCodeCell.Button1.Clicked += CardCodeButton_Clicked; - break; - case CipherType.Identity: - IdTitleCell.InitEvents(); - IdFirstNameCell.InitEvents(); - IdMiddleNameCell.InitEvents(); - IdLastNameCell.InitEvents(); - IdUsernameCell.InitEvents(); - IdCompanyCell.InitEvents(); - IdSsnCell.InitEvents(); - IdPassportNumberCell.InitEvents(); - IdLicenseNumberCell.InitEvents(); - IdEmailCell.InitEvents(); - IdPhoneCell.InitEvents(); - IdAddress1Cell.InitEvents(); - IdAddress2Cell.InitEvents(); - IdAddress3Cell.InitEvents(); - IdCityCell.InitEvents(); - IdStateCell.InitEvents(); - IdPostalCodeCell.InitEvents(); - IdCountryCell.InitEvents(); - break; - default: - break; - } - - Helpers.InitSectionEvents(FieldsSection); - Helpers.InitSectionEvents(UrisSection); - - if(_type == CipherType.Login && !_fromAutofill && !_settings.GetValueOrDefault(AddedLoginAlertKey, false)) - { - _settings.AddOrUpdateValue(AddedLoginAlertKey, true); - if(Device.RuntimePlatform == Device.iOS) - { - if(_deviceInfo.Version < 12) - { - DisplayAlert(AppResources.BitwardenAppExtension, AppResources.BitwardenAppExtensionAlert, - AppResources.Ok); - } - else - { - DisplayAlert(AppResources.PasswordAutofill, AppResources.BitwardenAutofillAlert, - AppResources.Ok); - } - } - else if(Device.RuntimePlatform == Device.Android && !_appInfoService.AutofillAccessibilityServiceEnabled) - { - DisplayAlert(AppResources.BitwardenAutofillService, AppResources.BitwardenAutofillServiceAlert, - AppResources.Ok); - } - } - - if(NameCell != null && string.IsNullOrWhiteSpace(NameCell.Entry.Text)) - { - NameCell.Entry.FocusWithDelay(); - } - else if(LoginUsernameCell != null && string.IsNullOrWhiteSpace(LoginUsernameCell.Entry.Text)) - { - LoginUsernameCell.Entry.FocusWithDelay(); - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - - NameCell.Dispose(); - NotesCell.Dispose(); - FolderCell.Dispose(); - - if(AddFieldCell != null) - { - AddFieldCell.Tapped -= AddFieldCell_Tapped; - } - if(AddUriCell != null) - { - AddUriCell.Tapped -= AddUriCell_Tapped; - } - - switch(_type) - { - case CipherType.Login: - LoginTotpCell.Dispose(); - LoginPasswordCell.Dispose(); - LoginUsernameCell.Dispose(); - LoginPasswordCell.Button1.Clicked -= PasswordButton_Clicked; - LoginPasswordCell.Button2.Clicked -= PasswordButton2_Clicked; - if(LoginTotpCell?.Button1 != null) - { - LoginTotpCell.Button1.Clicked -= TotpButton_Clicked; - } - break; - case CipherType.Card: - CardBrandCell.Dispose(); - CardCodeCell.Dispose(); - CardExpMonthCell.Dispose(); - CardExpYearCell.Dispose(); - CardNameCell.Dispose(); - CardNumberCell.Dispose(); - CardCodeCell.Button1.Clicked -= CardCodeButton_Clicked; - break; - case CipherType.Identity: - IdTitleCell.Dispose(); - IdFirstNameCell.Dispose(); - IdMiddleNameCell.Dispose(); - IdLastNameCell.Dispose(); - IdUsernameCell.Dispose(); - IdCompanyCell.Dispose(); - IdSsnCell.Dispose(); - IdPassportNumberCell.Dispose(); - IdLicenseNumberCell.Dispose(); - IdEmailCell.Dispose(); - IdPhoneCell.Dispose(); - IdAddress1Cell.Dispose(); - IdAddress2Cell.Dispose(); - IdAddress3Cell.Dispose(); - IdCityCell.Dispose(); - IdStateCell.Dispose(); - IdPostalCodeCell.Dispose(); - IdCountryCell.Dispose(); - break; - default: - break; - } - - Helpers.DisposeSectionEvents(FieldsSection); - Helpers.DisposeSectionEvents(UrisSection); - } - - protected override bool OnBackButtonPressed() - { - if(_fromAutofillFramework) - { - Application.Current.MainPage = new MainPage(); - return true; - } - - return base.OnBackButtonPressed(); - } - - private void PasswordButton_Clicked(object sender, EventArgs e) - { - LoginPasswordCell.Entry.InvokeToggleIsPassword(); - LoginPasswordCell.Button1.Image = - "eye" + (!LoginPasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - } - - private async void PasswordButton2_Clicked(object sender, EventArgs e) - { - var page = new ToolsPasswordGeneratorPage((password) => - { - LoginPasswordCell.Entry.Text = password; - _deviceActionService.Toast(AppResources.PasswordGenerated); - }, _fromAutofill); - await Navigation.PushForDeviceAsync(page); - } - - private async void TotpButton_Clicked(object sender, EventArgs e) - { - var scanPage = new ScanPage((key) => - { - Device.BeginInvokeOnMainThread(async () => - { - await Navigation.PopModalAsync(); - if(!string.IsNullOrWhiteSpace(key)) - { - LoginTotpCell.Entry.Text = key; - _deviceActionService.Toast(AppResources.AuthenticatorKeyAdded); - } - else - { - await DisplayAlert(null, AppResources.AuthenticatorKeyReadError, AppResources.Ok); - } - }); - }); - - await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage)); - } - - private void CardCodeButton_Clicked(object sender, EventArgs e) - { - CardCodeCell.Entry.InvokeToggleIsPassword(); - CardCodeCell.Button1.Image = - "eye" + (!CardCodeCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - - private void InitTable() - { - // Sections - TopSection = new TableSection(AppResources.ItemInformation) - { - NameCell - }; - - MiddleSection = new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - FolderCell, - FavoriteCell - }; - - if(_type == CipherType.Login) - { - LoginTotpCell = new FormEntryCell(AppResources.AuthenticatorKey, - button1: _deviceInfo.HasCamera ? "camera.png" : null); - LoginTotpCell.Entry.DisableAutocapitalize = true; - LoginTotpCell.Entry.Autocorrect = false; - LoginTotpCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - LoginPasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: LoginTotpCell.Entry, - button1: "eye.png", button2: "refresh_alt.png"); - LoginPasswordCell.Entry.DisableAutocapitalize = true; - LoginPasswordCell.Entry.Autocorrect = false; - LoginPasswordCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - if(!string.IsNullOrWhiteSpace(_defaultPassword)) - { - LoginPasswordCell.Entry.Text = _defaultPassword; - } - - LoginUsernameCell = new FormEntryCell(AppResources.Username, nextElement: LoginPasswordCell.Entry); - LoginUsernameCell.Entry.DisableAutocapitalize = true; - LoginUsernameCell.Entry.Autocorrect = false; - if(!string.IsNullOrWhiteSpace(_defaultUsername)) - { - LoginUsernameCell.Entry.Text = _defaultUsername; - } - - NameCell.NextElement = LoginUsernameCell.Entry; - - // Build sections - TopSection.Add(LoginUsernameCell); - TopSection.Add(LoginPasswordCell); - TopSection.Add(LoginTotpCell); - - // Uris - UrisSection = new TableSection(Helpers.GetEmptyTableSectionTitle()); - AddUriCell = new ExtendedTextCell - { - Text = $"+ {AppResources.NewUri}", - TextColor = Colors.Primary - }; - UrisSection.Add(AddUriCell); - UrisSection.Insert(0, Helpers.MakeUriCell(_defaultUri ?? string.Empty, null, UrisSection, this)); - } - else if(_type == CipherType.Card) - { - CardCodeCell = new FormEntryCell(AppResources.SecurityCode, Keyboard.Numeric, - isPassword: true, nextElement: NotesCell.Editor, button1: "eye.png"); - if(!string.IsNullOrWhiteSpace(_defaultCardCode)) - { - CardCodeCell.Entry.Text = _defaultCardCode; - } - CardCodeCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - CardExpYearCell = new FormEntryCell(AppResources.ExpirationYear, Keyboard.Numeric, - nextElement: CardCodeCell.Entry); - if(!string.IsNullOrWhiteSpace(_defaultCardExpYear)) - { - CardExpYearCell.Entry.Text = _defaultCardExpYear; - } - CardExpMonthCell = new FormPickerCell(AppResources.ExpirationMonth, new string[] { - "--", AppResources.January, AppResources.February, AppResources.March, AppResources.April, - AppResources.May, AppResources.June, AppResources.July, AppResources.August, AppResources.September, - AppResources.October, AppResources.November, AppResources.December - }); - if(_defaultCardExpMonth.HasValue) - { - CardExpMonthCell.Picker.SelectedIndex = _defaultCardExpMonth.Value; - } - CardBrandCell = new FormPickerCell(AppResources.Brand, new string[] { - "--", "Visa", "Mastercard", "American Express", "Discover", "Diners Club", - "JCB", "Maestro", "UnionPay", AppResources.Other - }); - CardNumberCell = new FormEntryCell(AppResources.Number, Keyboard.Numeric); - if(!string.IsNullOrWhiteSpace(_defaultCardNumber)) - { - CardNumberCell.Entry.Text = _defaultCardNumber; - } - CardNameCell = new FormEntryCell(AppResources.CardholderName, nextElement: CardNumberCell.Entry); - if(!string.IsNullOrWhiteSpace(_defaultCardName)) - { - CardNameCell.Entry.Text = _defaultCardName; - } - NameCell.NextElement = CardNameCell.Entry; - - // Build sections - TopSection.Add(CardNameCell); - TopSection.Add(CardNumberCell); - TopSection.Add(CardBrandCell); - TopSection.Add(CardExpMonthCell); - TopSection.Add(CardExpYearCell); - TopSection.Add(CardCodeCell); - } - else if(_type == CipherType.Identity) - { - IdCountryCell = new FormEntryCell(AppResources.Country, nextElement: NotesCell.Editor); - IdPostalCodeCell = new FormEntryCell(AppResources.ZipPostalCode, nextElement: IdCountryCell.Entry); - IdPostalCodeCell.Entry.DisableAutocapitalize = true; - IdPostalCodeCell.Entry.Autocorrect = false; - IdStateCell = new FormEntryCell(AppResources.StateProvince, nextElement: IdPostalCodeCell.Entry); - IdCityCell = new FormEntryCell(AppResources.CityTown, nextElement: IdStateCell.Entry); - IdAddress3Cell = new FormEntryCell(AppResources.Address3, nextElement: IdCityCell.Entry); - IdAddress2Cell = new FormEntryCell(AppResources.Address2, nextElement: IdAddress3Cell.Entry); - IdAddress1Cell = new FormEntryCell(AppResources.Address1, nextElement: IdAddress2Cell.Entry); - IdPhoneCell = new FormEntryCell(AppResources.Phone, nextElement: IdAddress1Cell.Entry); - IdPhoneCell.Entry.DisableAutocapitalize = true; - IdPhoneCell.Entry.Autocorrect = false; - IdEmailCell = new FormEntryCell(AppResources.Email, Keyboard.Email, nextElement: IdPhoneCell.Entry); - IdEmailCell.Entry.DisableAutocapitalize = true; - IdEmailCell.Entry.Autocorrect = false; - IdLicenseNumberCell = new FormEntryCell(AppResources.LicenseNumber, nextElement: IdEmailCell.Entry); - IdLicenseNumberCell.Entry.DisableAutocapitalize = true; - IdLicenseNumberCell.Entry.Autocorrect = false; - IdPassportNumberCell = new FormEntryCell(AppResources.PassportNumber, nextElement: IdLicenseNumberCell.Entry); - IdPassportNumberCell.Entry.DisableAutocapitalize = true; - IdPassportNumberCell.Entry.Autocorrect = false; - IdSsnCell = new FormEntryCell(AppResources.SSN, nextElement: IdPassportNumberCell.Entry); - IdSsnCell.Entry.DisableAutocapitalize = true; - IdSsnCell.Entry.Autocorrect = false; - IdCompanyCell = new FormEntryCell(AppResources.Company, nextElement: IdSsnCell.Entry); - IdUsernameCell = new FormEntryCell(AppResources.Username, nextElement: IdCompanyCell.Entry); - IdUsernameCell.Entry.DisableAutocapitalize = true; - IdUsernameCell.Entry.Autocorrect = false; - IdLastNameCell = new FormEntryCell(AppResources.LastName, nextElement: IdUsernameCell.Entry); - IdMiddleNameCell = new FormEntryCell(AppResources.MiddleName, nextElement: IdLastNameCell.Entry); - IdFirstNameCell = new FormEntryCell(AppResources.FirstName, nextElement: IdMiddleNameCell.Entry); - IdTitleCell = new FormPickerCell(AppResources.Title, new string[] { - "--", AppResources.Mr, AppResources.Mrs, AppResources.Ms, AppResources.Dr - }); - - // Name - NameCell.NextElement = IdFirstNameCell.Entry; - - // Build sections - TopSection.Add(IdTitleCell); - TopSection.Add(IdFirstNameCell); - TopSection.Add(IdMiddleNameCell); - TopSection.Add(IdLastNameCell); - TopSection.Add(IdUsernameCell); - TopSection.Add(IdCompanyCell); - TopSection.Add(IdSsnCell); - TopSection.Add(IdPassportNumberCell); - TopSection.Add(IdLicenseNumberCell); - TopSection.Add(IdEmailCell); - TopSection.Add(IdPhoneCell); - TopSection.Add(IdAddress1Cell); - TopSection.Add(IdAddress2Cell); - TopSection.Add(IdAddress3Cell); - TopSection.Add(IdCityCell); - TopSection.Add(IdStateCell); - TopSection.Add(IdPostalCodeCell); - TopSection.Add(IdCountryCell); - } - else if(_type == CipherType.SecureNote) - { - // Name - NameCell.NextElement = NotesCell.Editor; - } - - FieldsSection = new TableSection(AppResources.CustomFields); - AddFieldCell = new ExtendedTextCell - { - Text = $"+ {AppResources.NewCustomField}", - TextColor = Colors.Primary - }; - FieldsSection.Add(AddFieldCell); - - // Make table - TableRoot = new TableRoot - { - TopSection, - MiddleSection, - new TableSection(AppResources.Notes) - { - NotesCell - }, - FieldsSection - }; - - if(UrisSection != null) - { - TableRoot.Insert(1, UrisSection); - } - - Table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = true, - HasUnevenRows = true, - Root = TableRoot - }; - - if(Device.RuntimePlatform == Device.iOS) - { - Table.RowHeight = -1; - Table.EstimatedRowHeight = 70; - } - else if(Device.RuntimePlatform == Device.Android) - { - Table.BottomPadding = 50; - } - } - - private void InitSave() - { - var saveToolBarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(string.IsNullOrWhiteSpace(NameCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), AppResources.Ok); - return; - } - - var cipher = new Cipher - { - Name = NameCell.Entry.Text.Encrypt(), - Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null : NotesCell.Editor.Text.Encrypt(), - Favorite = FavoriteCell.On, - Type = _type - }; - - switch(_type) - { - case CipherType.Login: - cipher.Login = new Login - { - Username = string.IsNullOrWhiteSpace(LoginUsernameCell.Entry.Text) ? null : - LoginUsernameCell.Entry.Text.Encrypt(), - Password = string.IsNullOrWhiteSpace(LoginPasswordCell.Entry.Text) ? null : - LoginPasswordCell.Entry.Text.Encrypt(), - Totp = string.IsNullOrWhiteSpace(LoginTotpCell.Entry.Text) ? null : - LoginTotpCell.Entry.Text.Encrypt(), - }; - - Helpers.ProcessUrisSectionForSave(UrisSection, cipher); - break; - case CipherType.SecureNote: - cipher.SecureNote = new SecureNote - { - Type = SecureNoteType.Generic - }; - break; - case CipherType.Card: - string brand; - switch(CardBrandCell.Picker.SelectedIndex) - { - case 1: - brand = "Visa"; - break; - case 2: - brand = "Mastercard"; - break; - case 3: - brand = "Amex"; - break; - case 4: - brand = "Discover"; - break; - case 5: - brand = "Diners Club"; - break; - case 6: - brand = "JCB"; - break; - case 7: - brand = "Maestro"; - break; - case 8: - brand = "UnionPay"; - break; - case 9: - brand = "Other"; - break; - default: - brand = null; - break; - } - - var expMonth = CardExpMonthCell.Picker.SelectedIndex > 0 ? - CardExpMonthCell.Picker.SelectedIndex.ToString() : null; - - cipher.Card = new Card - { - CardholderName = string.IsNullOrWhiteSpace(CardNameCell.Entry.Text) ? null : - CardNameCell.Entry.Text.Encrypt(), - Number = string.IsNullOrWhiteSpace(CardNumberCell.Entry.Text) ? null : - CardNumberCell.Entry.Text.Encrypt(), - ExpYear = string.IsNullOrWhiteSpace(CardExpYearCell.Entry.Text) ? null : - CardExpYearCell.Entry.Text.Encrypt(), - Code = string.IsNullOrWhiteSpace(CardCodeCell.Entry.Text) ? null : - CardCodeCell.Entry.Text.Encrypt(), - Brand = string.IsNullOrWhiteSpace(brand) ? null : brand.Encrypt(), - ExpMonth = string.IsNullOrWhiteSpace(expMonth) ? null : expMonth.Encrypt(), - }; - break; - case CipherType.Identity: - string title; - switch(IdTitleCell.Picker.SelectedIndex) - { - case 1: - title = AppResources.Mr; - break; - case 2: - title = AppResources.Mrs; - break; - case 3: - title = AppResources.Ms; - break; - case 4: - title = AppResources.Dr; - break; - default: - title = null; - break; - } - - cipher.Identity = new Identity - { - Title = string.IsNullOrWhiteSpace(title) ? null : title.Encrypt(), - FirstName = string.IsNullOrWhiteSpace(IdFirstNameCell.Entry.Text) ? null : - IdFirstNameCell.Entry.Text.Encrypt(), - MiddleName = string.IsNullOrWhiteSpace(IdMiddleNameCell.Entry.Text) ? null : - IdMiddleNameCell.Entry.Text.Encrypt(), - LastName = string.IsNullOrWhiteSpace(IdLastNameCell.Entry.Text) ? null : - IdLastNameCell.Entry.Text.Encrypt(), - Username = string.IsNullOrWhiteSpace(IdUsernameCell.Entry.Text) ? null : - IdUsernameCell.Entry.Text.Encrypt(), - Company = string.IsNullOrWhiteSpace(IdCompanyCell.Entry.Text) ? null : - IdCompanyCell.Entry.Text.Encrypt(), - SSN = string.IsNullOrWhiteSpace(IdSsnCell.Entry.Text) ? null : - IdSsnCell.Entry.Text.Encrypt(), - PassportNumber = string.IsNullOrWhiteSpace(IdPassportNumberCell.Entry.Text) ? null : - IdPassportNumberCell.Entry.Text.Encrypt(), - LicenseNumber = string.IsNullOrWhiteSpace(IdLicenseNumberCell.Entry.Text) ? null : - IdLicenseNumberCell.Entry.Text.Encrypt(), - Email = string.IsNullOrWhiteSpace(IdEmailCell.Entry.Text) ? null : - IdEmailCell.Entry.Text.Encrypt(), - Phone = string.IsNullOrWhiteSpace(IdPhoneCell.Entry.Text) ? null : - IdPhoneCell.Entry.Text.Encrypt(), - Address1 = string.IsNullOrWhiteSpace(IdAddress1Cell.Entry.Text) ? null : - IdAddress1Cell.Entry.Text.Encrypt(), - Address2 = string.IsNullOrWhiteSpace(IdAddress2Cell.Entry.Text) ? null : - IdAddress2Cell.Entry.Text.Encrypt(), - Address3 = string.IsNullOrWhiteSpace(IdAddress3Cell.Entry.Text) ? null : - IdAddress3Cell.Entry.Text.Encrypt(), - City = string.IsNullOrWhiteSpace(IdCityCell.Entry.Text) ? null : - IdCityCell.Entry.Text.Encrypt(), - State = string.IsNullOrWhiteSpace(IdStateCell.Entry.Text) ? null : - IdStateCell.Entry.Text.Encrypt(), - PostalCode = string.IsNullOrWhiteSpace(IdPostalCodeCell.Entry.Text) ? null : - IdPostalCodeCell.Entry.Text.Encrypt(), - Country = string.IsNullOrWhiteSpace(IdCountryCell.Entry.Text) ? null : - IdCountryCell.Entry.Text.Encrypt() - }; - break; - default: - break; - } - - if(FolderCell.Picker.SelectedIndex > 0) - { - cipher.FolderId = Folders.ElementAt(FolderCell.Picker.SelectedIndex - 1).Id; - } - - Helpers.ProcessFieldsSectionForSave(FieldsSection, cipher); - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(cipher); - await _deviceActionService.HideLoadingAsync(); - - if(saveTask.Succeeded) - { - _deviceActionService.Toast(AppResources.NewItemCreated); - if(_fromAutofill) - { - _googleAnalyticsService.TrackExtensionEvent("CreatedCipher"); - } - else - { - _googleAnalyticsService.TrackAppEvent("CreatedCipher"); - } - - if(_fromAutofillFramework) - { - // close and go back to app - _deviceActionService.CloseAutofill(); - } - else - { - await Navigation.PopForDeviceAsync(); - } - } - else if(saveTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(saveToolBarItem); - } - - private async void AddFieldCell_Tapped(object sender, EventArgs e) - { - await Helpers.AddField(this, FieldsSection); - } - - private void AddUriCell_Tapped(object sender, EventArgs e) - { - var cell = Helpers.MakeUriCell(string.Empty, null, UrisSection, this); - if(cell != null) - { - UrisSection.Insert(UrisSection.Count - 1, cell); - cell.InitEvents(); - } - } - } -} diff --git a/src/App/Pages/Vault/VaultAttachmentsPage.cs b/src/App/Pages/Vault/VaultAttachmentsPage.cs deleted file mode 100644 index 6d5874195..000000000 --- a/src/App/Pages/Vault/VaultAttachmentsPage.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using Plugin.Connectivity.Abstractions; -using Bit.App.Models; -using System.Threading.Tasks; - -namespace Bit.App.Pages -{ - public class VaultAttachmentsPage : ExtendedContentPage - { - private readonly ICipherService _cipherService; - private readonly IConnectivity _connectivity; - private readonly IDeviceActionService _deviceActionService; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly ICryptoService _cryptoService; - private readonly string _cipherId; - private Cipher _cipher; - private byte[] _fileBytes; - private DateTime? _lastAction; - private bool _canUseAttachments = true; - - public VaultAttachmentsPage(string cipherId) - : base(true) - { - _cipherId = cipherId; - _cipherService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _cryptoService = Resolver.Resolve(); - - Init(); - } - - public ExtendedObservableCollection PresentationAttchments { get; private set; } - = new ExtendedObservableCollection(); - public ExtendedListView ListView { get; set; } - public RedrawableStackLayout NoDataStackLayout { get; set; } - public StackLayout AddNewStackLayout { get; set; } - public Label FileLabel { get; set; } - public ExtendedTableView NewTable { get; set; } - public Label NoDataLabel { get; set; } - public ToolbarItem SaveToolbarItem { get; set; } - - private void Init() - { - _canUseAttachments = _cryptoService.EncKey != null; - - SubscribeFileResult(true); - var selectButton = new ExtendedButton - { - Text = AppResources.ChooseFile, - Command = new Command(async () => await _deviceActionService.SelectFileAsync()), - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Button)) - }; - - FileLabel = new Label - { - Text = AppResources.NoFileChosen, - Style = (Style)Application.Current.Resources["text-muted"], - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - HorizontalTextAlignment = TextAlignment.Center - }; - - AddNewStackLayout = new StackLayout - { - Children = { selectButton, FileLabel }, - Orientation = StackOrientation.Vertical, - Padding = new Thickness(20, Helpers.OnPlatform(iOS: 10, Android: 20), 20, 20), - VerticalOptions = LayoutOptions.Start - }; - - NewTable = new ExtendedTableView - { - Intent = TableIntent.Settings, - HasUnevenRows = true, - NoFooter = true, - EnableScrolling = false, - EnableSelection = false, - VerticalOptions = LayoutOptions.Start, - Margin = new Thickness(0, Helpers.OnPlatform(iOS: 10, Android: 30), 0, 0), - WrappingStackLayout = () => NoDataStackLayout, - Root = new TableRoot - { - new TableSection(AppResources.AddNewAttachment) - { - new ExtendedViewCell - { - View = AddNewStackLayout, - BackgroundColor = Color.White - } - } - } - }; - - ListView = new ExtendedListView(ListViewCachingStrategy.RecycleElement) - { - ItemsSource = PresentationAttchments, - HasUnevenRows = true, - ItemTemplate = new DataTemplate(() => new VaultAttachmentsViewCell()), - VerticalOptions = LayoutOptions.FillAndExpand - }; - - NoDataLabel = new Label - { - Text = AppResources.NoAttachments, - HorizontalTextAlignment = TextAlignment.Center, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"] - }; - - NoDataStackLayout = new RedrawableStackLayout - { - VerticalOptions = LayoutOptions.Start, - Spacing = 0, - Margin = new Thickness(0, 40, 0, 0) - }; - - SaveToolbarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent() || _cipher == null) - { - return; - } - _lastAction = DateTime.UtcNow; - - - if(!_canUseAttachments) - { - await ShowUpdateKeyAsync(); - return; - } - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(_fileBytes == null) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.File), AppResources.Ok); - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.EncryptAndSaveAttachmentAsync(_cipher, _fileBytes, FileLabel.Text); - await _deviceActionService.HideLoadingAsync(); - - if(saveTask.Succeeded) - { - _fileBytes = null; - FileLabel.Text = AppResources.NoFileChosen; - _deviceActionService.Toast(AppResources.AttachementAdded); - _googleAnalyticsService.TrackAppEvent("AddedAttachment"); - await LoadAttachmentsAsync(); - } - else if(saveTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - - Title = AppResources.Attachments; - Content = ListView; - - if(Device.RuntimePlatform == Device.iOS) - { - ListView.RowHeight = -1; - NewTable.RowHeight = -1; - NewTable.EstimatedRowHeight = 44; - NewTable.HeightRequest = 180; - ListView.BackgroundColor = Color.Transparent; - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); - } - else if(Device.RuntimePlatform == Device.Android) - { - ListView.BottomPadding = 50; - } - } - - protected async override void OnAppearing() - { - base.OnAppearing(); - ListView.ItemSelected += AttachmentSelected; - await LoadAttachmentsAsync(); - - // Prevent from adding multiple save buttons - if(Device.RuntimePlatform == Device.iOS && ToolbarItems.Count > 1) - { - ToolbarItems.RemoveAt(1); - } - else if(Device.RuntimePlatform != Device.iOS && ToolbarItems.Count > 0) - { - ToolbarItems.RemoveAt(0); - } - - if(_cipher != null && (Helpers.CanAccessPremium() || _cipher.OrganizationId != null)) - { - ToolbarItems.Add(SaveToolbarItem); - ListView.Footer = NewTable; - - if(!_canUseAttachments) - { - await ShowUpdateKeyAsync(); - } - } - else - { - await DisplayAlert(null, AppResources.PremiumRequired, AppResources.Ok); - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - ListView.ItemSelected -= AttachmentSelected; - } - - private async Task LoadAttachmentsAsync() - { - _cipher = await _cipherService.GetByIdAsync(_cipherId); - if(_cipher == null) - { - await Navigation.PopForDeviceAsync(); - return; - } - - var attachmentsToAdd = _cipher.Attachments - .Select(a => new VaultAttachmentsPageModel.Attachment(a, _cipher.OrganizationId)) - .OrderBy(s => s.Name); - PresentationAttchments.ResetWithRange(attachmentsToAdd); - AdjustContent(); - } - - private void AdjustContent() - { - if(PresentationAttchments.Count == 0) - { - NoDataStackLayout.Children.Clear(); - NoDataStackLayout.Children.Add(NoDataLabel); - NoDataStackLayout.Children.Add(NewTable); - Content = NoDataStackLayout; - } - else - { - Content = ListView; - } - } - - private async void AttachmentSelected(object sender, SelectedItemChangedEventArgs e) - { - var attachment = e.SelectedItem as VaultAttachmentsPageModel.Attachment; - if(attachment == null) - { - return; - } - - ((ListView)sender).SelectedItem = null; - - var confirmed = await DisplayAlert(null, AppResources.DoYouReallyWantToDelete, AppResources.Yes, - AppResources.No); - if(!confirmed) - { - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); - var saveTask = await _cipherService.DeleteAttachmentAsync(_cipher, attachment.Id); - await _deviceActionService.HideLoadingAsync(); - - if(saveTask.Succeeded) - { - _deviceActionService.Toast(AppResources.AttachmentDeleted); - _googleAnalyticsService.TrackAppEvent("DeletedAttachment"); - await LoadAttachmentsAsync(); - } - else if(saveTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - - private void SubscribeFileResult(bool subscribe) - { - MessagingCenter.Unsubscribe>(Application.Current, "SelectFileResult"); - if(!subscribe) - { - return; - } - - MessagingCenter.Subscribe>( - Application.Current, "SelectFileResult", (sender, result) => - { - FileLabel.Text = result.Item2; - _fileBytes = result.Item1; - SubscribeFileResult(true); - }); - } - - private async Task ShowUpdateKeyAsync() - { - var confirmed = await DisplayAlert(AppResources.FeatureUnavailable, AppResources.UpdateKey, - AppResources.LearnMore, AppResources.Cancel); - if(confirmed) - { - Device.OpenUri(new Uri("https://help.bitwarden.com/article/update-encryption-key/")); - } - } - } -} diff --git a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs b/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs deleted file mode 100644 index 4b1d8edfa..000000000 --- a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using System.Threading; -using Bit.App.Models; -using System.Collections.Generic; -using Bit.App.Enums; -using static Bit.App.Models.Page.VaultListPageModel; -using Plugin.Connectivity.Abstractions; - -namespace Bit.App.Pages -{ - public class VaultAutofillListCiphersPage : ExtendedContentPage - { - private readonly ICipherService _cipherService; - private readonly IDeviceInfoService _deviceInfoService; - private readonly ISettingsService _settingsService; - private readonly IAppSettingsService _appSettingsService; - public readonly IConnectivity _connectivity; - private CancellationTokenSource _filterResultsCancellationTokenSource; - private readonly string _name; - private readonly AppOptions _appOptions; - - public VaultAutofillListCiphersPage(AppOptions appOptions) - : base(true) - { - _appOptions = appOptions; - Uri = appOptions.Uri; - if(Uri.StartsWith(Constants.AndroidAppProtocol)) - { - _name = Uri.Substring(Constants.AndroidAppProtocol.Length); - } - else if(!System.Uri.TryCreate(Uri, UriKind.Absolute, out Uri uri) || - !DomainName.TryParseBaseDomain(uri.Host, out _name)) - { - _name = "--"; - } - - _cipherService = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - DeviceActionService = Resolver.Resolve(); - _settingsService = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - GoogleAnalyticsService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - - Init(); - } - - public ContentView ContentView { get; set; } - public Fab Fab { get; set; } - public ExtendedObservableCollection> PresentationCiphersGroup { get; private set; } - = new ExtendedObservableCollection>(); - public StackLayout NoDataStackLayout { get; set; } - public ExtendedListView ListView { get; set; } - public ActivityIndicator LoadingIndicator { get; set; } - private SearchToolBarItem SearchItem { get; set; } - private IGoogleAnalyticsService GoogleAnalyticsService { get; set; } - private IDeviceActionService DeviceActionService { get; set; } - private string Uri { get; set; } - - private void Init() - { - var noDataLabel = new Label - { - Text = string.Format(AppResources.NoItemsForUri, _name ?? "--"), - HorizontalTextAlignment = TextAlignment.Center, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"] - }; - - var addCipherButton = new ExtendedButton - { - Text = AppResources.AddAnItem, - Command = new Command(() => AddCipherAsync()), - Style = (Style)Application.Current.Resources["btn-primaryAccent"] - }; - - NoDataStackLayout = new StackLayout - { - Children = { noDataLabel, addCipherButton }, - VerticalOptions = LayoutOptions.CenterAndExpand, - Padding = new Thickness(20, 0), - Spacing = 20 - }; - - SearchItem = new SearchToolBarItem(this); - ToolbarItems.Add(SearchItem); - - ListView = new ExtendedListView(ListViewCachingStrategy.RecycleElement) - { - IsGroupingEnabled = true, - ItemsSource = PresentationCiphersGroup, - HasUnevenRows = true, - GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( - nameof(Section.Name))), - ItemTemplate = new DataTemplate(() => new VaultListViewCell( - (VaultListPageModel.Cipher c) => Helpers.CipherMoreClickedAsync(this, c, true))) - }; - - if(Device.RuntimePlatform == Device.iOS) - { - ListView.RowHeight = -1; - } - - Title = string.Format(AppResources.ItemsForUri, _name ?? "--"); - - LoadingIndicator = new ActivityIndicator - { - IsRunning = true, - VerticalOptions = LayoutOptions.CenterAndExpand, - HorizontalOptions = LayoutOptions.Center - }; - - ContentView = new ContentView - { - Content = LoadingIndicator - }; - - var fabLayout = new FabLayout(ContentView); - Fab = new Fab(fabLayout, "plus.png", async (sender, args) => await AddCipherAsync()); - ListView.BottomPadding = 170; - - Content = fabLayout; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - ListView.ItemSelected += CipherSelected; - SearchItem.InitEvents(); - _filterResultsCancellationTokenSource = FetchAndLoadVault(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - ListView.ItemSelected -= CipherSelected; - SearchItem.Dispose(); - } - - protected override bool OnBackButtonPressed() - { - GoogleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App"); - DeviceActionService.CloseAutofill(); - return true; - } - - private void AdjustContent() - { - if(PresentationCiphersGroup.Count > 0) - { - ContentView.Content = ListView; - } - else - { - ContentView.Content = NoDataStackLayout; - } - } - - private CancellationTokenSource FetchAndLoadVault() - { - var cts = new CancellationTokenSource(); - _filterResultsCancellationTokenSource?.Cancel(); - - Task.Run(async () => - { - var autofillGroupings = new List>(); - var ciphers = await _cipherService.GetAllAsync(Uri); - - if(_appOptions.FillType.HasValue && _appOptions.FillType.Value != CipherType.Login) - { - var others = ciphers?.Item3.Where(c => c.Type == _appOptions.FillType.Value) - .Select(c => new AutofillCipher(c, _appSettingsService, false)) - .OrderBy(s => s.Name) - .ThenBy(s => s.Subtitle) - .ToList(); - if(others?.Any() ?? false) - { - autofillGroupings.Add(new Section(others, AppResources.Items)); - } - } - else - { - var normalLogins = ciphers?.Item1 - .Select(l => new AutofillCipher(l, _appSettingsService, false)) - .OrderBy(s => s.Name) - .ThenBy(s => s.Subtitle) - .ToList(); - if(normalLogins?.Any() ?? false) - { - autofillGroupings.Add(new Section(normalLogins, - AppResources.MatchingItems)); - } - - var fuzzyLogins = ciphers?.Item2 - .Select(l => new AutofillCipher(l, _appSettingsService, true)) - .OrderBy(s => s.Name) - .ThenBy(s => s.Subtitle) - .ToList(); - if(fuzzyLogins?.Any() ?? false) - { - autofillGroupings.Add(new Section(fuzzyLogins, - AppResources.PossibleMatchingItems)); - } - } - - Device.BeginInvokeOnMainThread(() => - { - if(autofillGroupings.Any()) - { - PresentationCiphersGroup.ResetWithRange(autofillGroupings); - } - - AdjustContent(); - }); - }, cts.Token); - - return cts; - } - - private async void CipherSelected(object sender, SelectedItemChangedEventArgs e) - { - var cipher = e.SelectedItem as AutofillCipher; - if(cipher == null) - { - return; - } - - if(_deviceInfoService.Version < 21) - { - Helpers.CipherMoreClickedAsync(this, cipher, true); - } - else - { - var autofillResponse = AppResources.Yes; - if(cipher.Fuzzy) - { - var options = new List { AppResources.Yes }; - if(cipher.Type == CipherType.Login && _connectivity.IsConnected) - { - options.Add(AppResources.YesAndSave); - } - - autofillResponse = await DeviceActionService.DisplayAlertAsync(null, - string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, _name), AppResources.No, - options.ToArray()); - } - - if(autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login) - { - if(!_connectivity.IsConnected) - { - Helpers.AlertNoConnection(this); - } - else - { - var uris = cipher.CipherModel.Login?.Uris?.ToList(); - if(uris == null) - { - uris = new List(); - } - - uris.Add(new LoginUri - { - Uri = Uri.Encrypt(cipher.CipherModel.OrganizationId), - Match = null - }); - - cipher.CipherModel.Login.Uris = uris; - - await DeviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); - await DeviceActionService.HideLoadingAsync(); - - if(saveTask.Succeeded) - { - GoogleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); - } - } - } - - if(autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave) - { - GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", - Uri.StartsWith("http") ? "Website" : "App"); - DeviceActionService.Autofill(cipher); - } - } - - ((ListView)sender).SelectedItem = null; - } - - private async Task AddCipherAsync() - { - if(_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login) - { - var pageForOther = new VaultAddCipherPage(_appOptions.FillType.Value, null, null, true); - await Navigation.PushForDeviceAsync(pageForOther); - return; - } - - var pageForLogin = new VaultAddCipherPage(CipherType.Login, Uri, _name, true); - await Navigation.PushForDeviceAsync(pageForLogin); - } - - private class SearchToolBarItem : ExtendedToolbarItem - { - private readonly VaultAutofillListCiphersPage _page; - - public SearchToolBarItem(VaultAutofillListCiphersPage page) - { - _page = page; - Text = AppResources.Search; - Icon = "search.png"; - Priority = 1; - ClickAction = () => DoClick(); - } - - private void DoClick() - { - _page.GoogleAnalyticsService.TrackExtensionEvent("CloseToSearch", - _page.Uri.StartsWith("http") ? "Website" : "App"); - Application.Current.MainPage = new ExtendedNavigationPage(new VaultListCiphersPage(uri: _page.Uri)); - _page.DeviceActionService.Toast(string.Format(AppResources.BitwardenAutofillServiceSearch, _page._name), - true); - } - } - } -} diff --git a/src/App/Pages/Vault/VaultEditCipherPage.cs b/src/App/Pages/Vault/VaultEditCipherPage.cs deleted file mode 100644 index c962cbf1d..000000000 --- a/src/App/Pages/Vault/VaultEditCipherPage.cs +++ /dev/null @@ -1,981 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using Bit.App.Models; -using Bit.App.Enums; - -namespace Bit.App.Pages -{ - public class VaultEditCipherPage : ExtendedContentPage - { - private readonly string _cipherId; - private readonly ICipherService _cipherService; - private readonly IFolderService _folderService; - private readonly IDeviceActionService _deviceActionService; - private readonly IConnectivity _connectivity; - private readonly IDeviceInfoService _deviceInfo; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private DateTime? _lastAction; - private string _originalLoginPassword = null; - private List> _originalHiddenFields = new List>(); - - public VaultEditCipherPage(string cipherId) - { - _cipherId = cipherId; - _cipherService = Resolver.Resolve(); - _folderService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _deviceInfo = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public Cipher Cipher { get; set; } - public List Folders { get; set; } - public TableRoot TableRoot { get; set; } - public TableSection TopSection { get; set; } - public TableSection UrisSection { get; set; } - public TableSection MiddleSection { get; set; } - public TableSection FieldsSection { get; set; } - public ExtendedTableView Table { get; set; } - - public FormEntryCell NameCell { get; private set; } - public FormEditorCell NotesCell { get; private set; } - public FormPickerCell FolderCell { get; private set; } - public ExtendedSwitchCell FavoriteCell { get; set; } - public ExtendedTextCell AttachmentsCell { get; private set; } - public ExtendedTextCell DeleteCell { get; private set; } - public ExtendedTextCell AddFieldCell { get; private set; } - public ExtendedTextCell AddUriCell { get; private set; } - - // Login - public FormEntryCell LoginPasswordCell { get; private set; } - public FormEntryCell LoginUsernameCell { get; private set; } - public FormEntryCell LoginTotpCell { get; private set; } - - // Card - public FormEntryCell CardNameCell { get; private set; } - public FormEntryCell CardNumberCell { get; private set; } - public FormPickerCell CardBrandCell { get; private set; } - public FormPickerCell CardExpMonthCell { get; private set; } - public FormEntryCell CardExpYearCell { get; private set; } - public FormEntryCell CardCodeCell { get; private set; } - - // Identity - public FormPickerCell IdTitleCell { get; private set; } - public FormEntryCell IdFirstNameCell { get; private set; } - public FormEntryCell IdMiddleNameCell { get; private set; } - public FormEntryCell IdLastNameCell { get; private set; } - public FormEntryCell IdUsernameCell { get; private set; } - public FormEntryCell IdCompanyCell { get; private set; } - public FormEntryCell IdSsnCell { get; private set; } - public FormEntryCell IdPassportNumberCell { get; private set; } - public FormEntryCell IdLicenseNumberCell { get; private set; } - public FormEntryCell IdEmailCell { get; private set; } - public FormEntryCell IdPhoneCell { get; private set; } - public FormEntryCell IdAddress1Cell { get; private set; } - public FormEntryCell IdAddress2Cell { get; private set; } - public FormEntryCell IdAddress3Cell { get; private set; } - public FormEntryCell IdCityCell { get; private set; } - public FormEntryCell IdStateCell { get; private set; } - public FormEntryCell IdPostalCodeCell { get; private set; } - public FormEntryCell IdCountryCell { get; private set; } - - private void Init() - { - Cipher = _cipherService.GetByIdAsync(_cipherId).GetAwaiter().GetResult(); - if(Cipher == null) - { - // TODO: handle error. navigate back? should never happen... - return; - } - - // Name - NameCell = new FormEntryCell(AppResources.Name); - NameCell.Entry.Text = Cipher.Name?.Decrypt(Cipher.OrganizationId); - - // Notes - NotesCell = new FormEditorCell(Keyboard.Text, Cipher.Type == CipherType.SecureNote ? 500 : 180); - NotesCell.Editor.Text = Cipher.Notes?.Decrypt(Cipher.OrganizationId); - - // Folders - var folderOptions = new List { AppResources.FolderNone }; - Folders = _folderService.GetAllAsync().GetAwaiter().GetResult() - .OrderBy(f => f.Name?.Decrypt()).ToList(); - int selectedIndex = 0; - int i = 0; - foreach(var folder in Folders) - { - i++; - if(folder.Id == Cipher.FolderId) - { - selectedIndex = i; - } - - folderOptions.Add(folder.Name.Decrypt()); - } - FolderCell = new FormPickerCell(AppResources.Folder, folderOptions.ToArray()); - FolderCell.Picker.SelectedIndex = selectedIndex; - - // Favorite - FavoriteCell = new ExtendedSwitchCell - { - Text = AppResources.Favorite, - On = Cipher.Favorite - }; - - // Delete - DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red }; - - InitTable(); - InitSave(); - - Title = AppResources.EditItem; - Content = Table; - if(Device.RuntimePlatform == Device.iOS) - { - ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); - } - } - - private void InitTable() - { - AttachmentsCell = new ExtendedTextCell - { - Text = AppResources.Attachments, - ShowDisclousure = true - }; - - // Sections - TopSection = new TableSection(AppResources.ItemInformation) - { - NameCell - }; - - MiddleSection = new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - FolderCell, - FavoriteCell, - AttachmentsCell - }; - - // Types - if(Cipher.Type == CipherType.Login) - { - LoginTotpCell = new FormEntryCell(AppResources.AuthenticatorKey, - button1: _deviceInfo.HasCamera ? "camera.png" : null); - LoginTotpCell.Entry.Text = Cipher.Login?.Totp?.Decrypt(Cipher.OrganizationId); - LoginTotpCell.Entry.DisableAutocapitalize = true; - LoginTotpCell.Entry.Autocorrect = false; - LoginTotpCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - LoginPasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, - nextElement: LoginTotpCell.Entry, button1: "eye.png", button2: "refresh_alt.png"); - LoginPasswordCell.Entry.Text = _originalLoginPassword = - Cipher.Login?.Password?.Decrypt(Cipher.OrganizationId); - LoginPasswordCell.Entry.DisableAutocapitalize = true; - LoginPasswordCell.Entry.Autocorrect = false; - LoginPasswordCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - LoginUsernameCell = new FormEntryCell(AppResources.Username, nextElement: LoginPasswordCell.Entry); - LoginUsernameCell.Entry.Text = Cipher.Login?.Username?.Decrypt(Cipher.OrganizationId); - LoginUsernameCell.Entry.DisableAutocapitalize = true; - LoginUsernameCell.Entry.Autocorrect = false; - - // Name - NameCell.NextElement = LoginUsernameCell.Entry; - - // Build sections - TopSection.Add(LoginUsernameCell); - TopSection.Add(LoginPasswordCell); - TopSection.Add(LoginTotpCell); - - // Uris - UrisSection = new TableSection(Helpers.GetEmptyTableSectionTitle()); - AddUriCell = new ExtendedTextCell - { - Text = $"+ {AppResources.NewUri}", - TextColor = Colors.Primary - }; - UrisSection.Add(AddUriCell); - if(Cipher.Login?.Uris != null) - { - foreach(var uri in Cipher.Login.Uris) - { - var value = uri.Uri?.Decrypt(Cipher.OrganizationId); - UrisSection.Insert(UrisSection.Count - 1, - Helpers.MakeUriCell(value, uri.Match, UrisSection, this)); - } - } - } - else if(Cipher.Type == CipherType.Card) - { - CardCodeCell = new FormEntryCell(AppResources.SecurityCode, Keyboard.Numeric, - isPassword: true, nextElement: NotesCell.Editor, button1: "eye.png"); - CardCodeCell.Entry.Text = Cipher.Card.Code?.Decrypt(Cipher.OrganizationId); - CardCodeCell.Entry.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - CardExpYearCell = new FormEntryCell(AppResources.ExpirationYear, Keyboard.Numeric, - nextElement: CardCodeCell.Entry); - CardExpYearCell.Entry.Text = Cipher.Card.ExpYear?.Decrypt(Cipher.OrganizationId); - - var month = Cipher.Card.ExpMonth?.Decrypt(Cipher.OrganizationId); - CardExpMonthCell = new FormPickerCell(AppResources.ExpirationMonth, new string[] { - "--", AppResources.January, AppResources.February, AppResources.March, AppResources.April, - AppResources.May, AppResources.June, AppResources.July, AppResources.August, AppResources.September, - AppResources.October, AppResources.November, AppResources.December - }); - if(!string.IsNullOrWhiteSpace(month) && int.TryParse(month, out int monthIndex)) - { - CardExpMonthCell.Picker.SelectedIndex = monthIndex; - } - else - { - CardExpMonthCell.Picker.SelectedIndex = 0; - } - - var brandOptions = new string[] { - "--", "Visa", "Mastercard", "American Express", "Discover", "Diners Club", - "JCB", "Maestro", "UnionPay", AppResources.Other - }; - var brand = Cipher.Card.Brand?.Decrypt(Cipher.OrganizationId); - CardBrandCell = new FormPickerCell(AppResources.Brand, brandOptions); - CardBrandCell.Picker.SelectedIndex = 0; - if(!string.IsNullOrWhiteSpace(brand)) - { - var i = 0; - foreach(var o in brandOptions) - { - var option = o; - if(option == AppResources.Other) - { - option = "Other"; - } - - if(option == brand) - { - CardBrandCell.Picker.SelectedIndex = i; - break; - } - i++; - } - } - - CardNumberCell = new FormEntryCell(AppResources.Number, Keyboard.Numeric); - CardNumberCell.Entry.Text = Cipher.Card.Number?.Decrypt(Cipher.OrganizationId); - - CardNameCell = new FormEntryCell(AppResources.CardholderName, nextElement: CardNumberCell.Entry); - CardNameCell.Entry.Text = Cipher.Card.CardholderName?.Decrypt(Cipher.OrganizationId); - - // Name - NameCell.NextElement = CardNameCell.Entry; - - // Build sections - TopSection.Add(CardNameCell); - TopSection.Add(CardNumberCell); - TopSection.Add(CardBrandCell); - TopSection.Add(CardExpMonthCell); - TopSection.Add(CardExpYearCell); - TopSection.Add(CardCodeCell); - } - else if(Cipher.Type == CipherType.Identity) - { - IdCountryCell = new FormEntryCell(AppResources.Country, nextElement: NotesCell.Editor); - IdCountryCell.Entry.Text = Cipher.Identity.Country?.Decrypt(Cipher.OrganizationId); - - IdPostalCodeCell = new FormEntryCell(AppResources.ZipPostalCode, nextElement: IdCountryCell.Entry); - IdPostalCodeCell.Entry.Text = Cipher.Identity.PostalCode?.Decrypt(Cipher.OrganizationId); - IdPostalCodeCell.Entry.DisableAutocapitalize = true; - IdPostalCodeCell.Entry.Autocorrect = false; - - IdStateCell = new FormEntryCell(AppResources.StateProvince, nextElement: IdPostalCodeCell.Entry); - IdStateCell.Entry.Text = Cipher.Identity.State?.Decrypt(Cipher.OrganizationId); - - IdCityCell = new FormEntryCell(AppResources.CityTown, nextElement: IdStateCell.Entry); - IdCityCell.Entry.Text = Cipher.Identity.City?.Decrypt(Cipher.OrganizationId); - - IdAddress3Cell = new FormEntryCell(AppResources.Address3, nextElement: IdCityCell.Entry); - IdAddress3Cell.Entry.Text = Cipher.Identity.Address3?.Decrypt(Cipher.OrganizationId); - - IdAddress2Cell = new FormEntryCell(AppResources.Address2, nextElement: IdAddress3Cell.Entry); - IdAddress2Cell.Entry.Text = Cipher.Identity.Address2?.Decrypt(Cipher.OrganizationId); - - IdAddress1Cell = new FormEntryCell(AppResources.Address1, nextElement: IdAddress2Cell.Entry); - IdAddress1Cell.Entry.Text = Cipher.Identity.Address1?.Decrypt(Cipher.OrganizationId); - - IdPhoneCell = new FormEntryCell(AppResources.Phone, nextElement: IdAddress1Cell.Entry); - IdPhoneCell.Entry.Text = Cipher.Identity.Phone?.Decrypt(Cipher.OrganizationId); - IdPhoneCell.Entry.DisableAutocapitalize = true; - IdPhoneCell.Entry.Autocorrect = false; - - IdEmailCell = new FormEntryCell(AppResources.Email, Keyboard.Email, nextElement: IdPhoneCell.Entry); - IdEmailCell.Entry.Text = Cipher.Identity.Email?.Decrypt(Cipher.OrganizationId); - IdEmailCell.Entry.DisableAutocapitalize = true; - IdEmailCell.Entry.Autocorrect = false; - - IdLicenseNumberCell = new FormEntryCell(AppResources.LicenseNumber, nextElement: IdEmailCell.Entry); - IdLicenseNumberCell.Entry.Text = Cipher.Identity.LicenseNumber?.Decrypt(Cipher.OrganizationId); - IdLicenseNumberCell.Entry.DisableAutocapitalize = true; - IdLicenseNumberCell.Entry.Autocorrect = false; - - IdPassportNumberCell = new FormEntryCell(AppResources.PassportNumber, nextElement: IdLicenseNumberCell.Entry); - IdPassportNumberCell.Entry.Text = Cipher.Identity.PassportNumber?.Decrypt(Cipher.OrganizationId); - IdPassportNumberCell.Entry.DisableAutocapitalize = true; - IdPassportNumberCell.Entry.Autocorrect = false; - - IdSsnCell = new FormEntryCell(AppResources.SSN, nextElement: IdPassportNumberCell.Entry); - IdSsnCell.Entry.Text = Cipher.Identity.SSN?.Decrypt(Cipher.OrganizationId); - IdSsnCell.Entry.DisableAutocapitalize = true; - IdSsnCell.Entry.Autocorrect = false; - - IdCompanyCell = new FormEntryCell(AppResources.Company, nextElement: IdSsnCell.Entry); - IdCompanyCell.Entry.Text = Cipher.Identity.Company?.Decrypt(Cipher.OrganizationId); - - IdUsernameCell = new FormEntryCell(AppResources.Username, nextElement: IdCompanyCell.Entry); - IdUsernameCell.Entry.Text = Cipher.Identity.Username?.Decrypt(Cipher.OrganizationId); - IdUsernameCell.Entry.DisableAutocapitalize = true; - IdUsernameCell.Entry.Autocorrect = false; - - IdLastNameCell = new FormEntryCell(AppResources.LastName, nextElement: IdUsernameCell.Entry); - IdLastNameCell.Entry.Text = Cipher.Identity.LastName?.Decrypt(Cipher.OrganizationId); - - IdMiddleNameCell = new FormEntryCell(AppResources.MiddleName, nextElement: IdLastNameCell.Entry); - IdMiddleNameCell.Entry.Text = Cipher.Identity.MiddleName?.Decrypt(Cipher.OrganizationId); - - IdFirstNameCell = new FormEntryCell(AppResources.FirstName, nextElement: IdMiddleNameCell.Entry); - IdFirstNameCell.Entry.Text = Cipher.Identity.FirstName?.Decrypt(Cipher.OrganizationId); - - var titleOptions = new string[] { - "--", AppResources.Mr, AppResources.Mrs, AppResources.Ms, AppResources.Dr - }; - IdTitleCell = new FormPickerCell(AppResources.Title, titleOptions); - var title = Cipher.Identity.Title?.Decrypt(Cipher.OrganizationId); - IdTitleCell.Picker.SelectedIndex = 0; - if(!string.IsNullOrWhiteSpace(title)) - { - var i = 0; - foreach(var o in titleOptions) - { - i++; - if(o == title) - { - IdTitleCell.Picker.SelectedIndex = i; - break; - } - } - } - - // Name - NameCell.NextElement = IdFirstNameCell.Entry; - - // Build sections - TopSection.Add(IdTitleCell); - TopSection.Add(IdFirstNameCell); - TopSection.Add(IdMiddleNameCell); - TopSection.Add(IdLastNameCell); - TopSection.Add(IdUsernameCell); - TopSection.Add(IdCompanyCell); - TopSection.Add(IdSsnCell); - TopSection.Add(IdPassportNumberCell); - TopSection.Add(IdLicenseNumberCell); - TopSection.Add(IdEmailCell); - TopSection.Add(IdPhoneCell); - TopSection.Add(IdAddress1Cell); - TopSection.Add(IdAddress2Cell); - TopSection.Add(IdAddress3Cell); - TopSection.Add(IdCityCell); - TopSection.Add(IdStateCell); - TopSection.Add(IdPostalCodeCell); - TopSection.Add(IdCountryCell); - } - else if(Cipher.Type == CipherType.SecureNote) - { - // Name - NameCell.NextElement = NotesCell.Editor; - } - - FieldsSection = new TableSection(AppResources.CustomFields); - if(Cipher.Fields != null) - { - foreach(var field in Cipher.Fields) - { - var label = field.Name?.Decrypt(Cipher.OrganizationId) ?? string.Empty; - var value = field.Value?.Decrypt(Cipher.OrganizationId); - var cell = Helpers.MakeFieldCell(field.Type, label, value, FieldsSection, this); - if(cell != null) - { - FieldsSection.Add(cell); - } - if(!string.IsNullOrWhiteSpace(label) && !string.IsNullOrWhiteSpace(value) && - field.Type == FieldType.Hidden) - { - _originalHiddenFields.Add(new Tuple(label, value)); - } - } - } - AddFieldCell = new ExtendedTextCell - { - Text = $"+ {AppResources.NewCustomField}", - TextColor = Colors.Primary - }; - FieldsSection.Add(AddFieldCell); - - // Make table - TableRoot = new TableRoot - { - TopSection, - MiddleSection, - new TableSection(AppResources.Notes) - { - NotesCell - }, - FieldsSection, - new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - DeleteCell - } - }; - - if(UrisSection != null) - { - TableRoot.Insert(1, UrisSection); - } - - Table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = true, - HasUnevenRows = true, - Root = TableRoot - }; - - if(Device.RuntimePlatform == Device.iOS) - { - Table.RowHeight = -1; - Table.EstimatedRowHeight = 70; - } - else if(Device.RuntimePlatform == Device.Android) - { - Table.BottomPadding = 50; - } - } - - private void InitSave() - { - var saveToolBarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent()) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(string.IsNullOrWhiteSpace(NameCell.Entry.Text)) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), AppResources.Ok); - return; - } - - Cipher.Name = NameCell.Entry.Text.Encrypt(Cipher.OrganizationId); - Cipher.Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null : - NotesCell.Editor.Text.Encrypt(Cipher.OrganizationId); - Cipher.Favorite = FavoriteCell.On; - - var passwordHistory = Cipher.PasswordHistory?.ToList() ?? new List(); - switch(Cipher.Type) - { - case CipherType.Login: - Cipher.Login = new Login - { - Username = string.IsNullOrWhiteSpace(LoginUsernameCell.Entry.Text) ? null : - LoginUsernameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Password = string.IsNullOrWhiteSpace(LoginPasswordCell.Entry.Text) ? null : - LoginPasswordCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Totp = string.IsNullOrWhiteSpace(LoginTotpCell.Entry.Text) ? null : - LoginTotpCell.Entry.Text.Encrypt(Cipher.OrganizationId), - PasswordRevisionDate = Cipher.Login.PasswordRevisionDate, - }; - - if(!string.IsNullOrWhiteSpace(_originalLoginPassword) && - LoginPasswordCell.Entry.Text != _originalLoginPassword) - { - var now = DateTime.UtcNow; - passwordHistory.Insert(0, new PasswordHistory - { - LastUsedDate = now, - Password = _originalLoginPassword.Encrypt(Cipher.OrganizationId), - }); - Cipher.Login.PasswordRevisionDate = now; - } - - Helpers.ProcessUrisSectionForSave(UrisSection, Cipher); - break; - case CipherType.SecureNote: - Cipher.SecureNote = new SecureNote - { - Type = SecureNoteType.Generic - }; - break; - case CipherType.Card: - string brand; - switch(CardBrandCell.Picker.SelectedIndex) - { - case 1: - brand = "Visa"; - break; - case 2: - brand = "Mastercard"; - break; - case 3: - brand = "Amex"; - break; - case 4: - brand = "Discover"; - break; - case 5: - brand = "Diners Club"; - break; - case 6: - brand = "JCB"; - break; - case 7: - brand = "Maestro"; - break; - case 8: - brand = "UnionPay"; - break; - case 9: - brand = "Other"; - break; - default: - brand = null; - break; - } - - var expMonth = CardExpMonthCell.Picker.SelectedIndex > 0 ? - CardExpMonthCell.Picker.SelectedIndex.ToString() : null; - - Cipher.Card = new Card - { - CardholderName = string.IsNullOrWhiteSpace(CardNameCell.Entry.Text) ? null : - CardNameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Number = string.IsNullOrWhiteSpace(CardNumberCell.Entry.Text) ? null : - CardNumberCell.Entry.Text.Encrypt(Cipher.OrganizationId), - ExpYear = string.IsNullOrWhiteSpace(CardExpYearCell.Entry.Text) ? null : - CardExpYearCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Code = string.IsNullOrWhiteSpace(CardCodeCell.Entry.Text) ? null : - CardCodeCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Brand = string.IsNullOrWhiteSpace(brand) ? null : brand.Encrypt(Cipher.OrganizationId), - ExpMonth = string.IsNullOrWhiteSpace(expMonth) ? null : expMonth.Encrypt(Cipher.OrganizationId) - }; - break; - case CipherType.Identity: - string title; - switch(IdTitleCell.Picker.SelectedIndex) - { - case 1: - title = AppResources.Mr; - break; - case 2: - title = AppResources.Mrs; - break; - case 3: - title = AppResources.Ms; - break; - case 4: - title = AppResources.Dr; - break; - default: - title = null; - break; - } - - Cipher.Identity = new Identity - { - Title = string.IsNullOrWhiteSpace(title) ? null : title.Encrypt(Cipher.OrganizationId), - FirstName = string.IsNullOrWhiteSpace(IdFirstNameCell.Entry.Text) ? null : - IdFirstNameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - MiddleName = string.IsNullOrWhiteSpace(IdMiddleNameCell.Entry.Text) ? null : - IdMiddleNameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - LastName = string.IsNullOrWhiteSpace(IdLastNameCell.Entry.Text) ? null : - IdLastNameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Username = string.IsNullOrWhiteSpace(IdUsernameCell.Entry.Text) ? null : - IdUsernameCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Company = string.IsNullOrWhiteSpace(IdCompanyCell.Entry.Text) ? null : - IdCompanyCell.Entry.Text.Encrypt(Cipher.OrganizationId), - SSN = string.IsNullOrWhiteSpace(IdSsnCell.Entry.Text) ? null : - IdSsnCell.Entry.Text.Encrypt(Cipher.OrganizationId), - PassportNumber = string.IsNullOrWhiteSpace(IdPassportNumberCell.Entry.Text) ? null : - IdPassportNumberCell.Entry.Text.Encrypt(Cipher.OrganizationId), - LicenseNumber = string.IsNullOrWhiteSpace(IdLicenseNumberCell.Entry.Text) ? null : - IdLicenseNumberCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Email = string.IsNullOrWhiteSpace(IdEmailCell.Entry.Text) ? null : - IdEmailCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Phone = string.IsNullOrWhiteSpace(IdPhoneCell.Entry.Text) ? null : - IdPhoneCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Address1 = string.IsNullOrWhiteSpace(IdAddress1Cell.Entry.Text) ? null : - IdAddress1Cell.Entry.Text.Encrypt(Cipher.OrganizationId), - Address2 = string.IsNullOrWhiteSpace(IdAddress2Cell.Entry.Text) ? null : - IdAddress2Cell.Entry.Text.Encrypt(Cipher.OrganizationId), - Address3 = string.IsNullOrWhiteSpace(IdAddress3Cell.Entry.Text) ? null : - IdAddress3Cell.Entry.Text.Encrypt(Cipher.OrganizationId), - City = string.IsNullOrWhiteSpace(IdCityCell.Entry.Text) ? null : - IdCityCell.Entry.Text.Encrypt(Cipher.OrganizationId), - State = string.IsNullOrWhiteSpace(IdStateCell.Entry.Text) ? null : - IdStateCell.Entry.Text.Encrypt(Cipher.OrganizationId), - PostalCode = string.IsNullOrWhiteSpace(IdPostalCodeCell.Entry.Text) ? null : - IdPostalCodeCell.Entry.Text.Encrypt(Cipher.OrganizationId), - Country = string.IsNullOrWhiteSpace(IdCountryCell.Entry.Text) ? null : - IdCountryCell.Entry.Text.Encrypt(Cipher.OrganizationId) - }; - break; - default: - break; - } - - if(FolderCell.Picker.SelectedIndex > 0) - { - Cipher.FolderId = Folders.ElementAt(FolderCell.Picker.SelectedIndex - 1).Id; - } - else - { - Cipher.FolderId = null; - } - - var hiddenFields = Helpers.ProcessFieldsSectionForSave(FieldsSection, Cipher); - var changedFields = _originalHiddenFields.Where(of => - hiddenFields.Any(f => f.Item1 == of.Item1 && f.Item2 != of.Item2)); - foreach(var cf in changedFields) - { - passwordHistory.Insert(0, new PasswordHistory - { - LastUsedDate = DateTime.UtcNow, - Password = (cf.Item1 + ": " + cf.Item2).Encrypt(Cipher.OrganizationId), - }); - } - Cipher.PasswordHistory = (passwordHistory?.Count ?? 0) > 0 ? passwordHistory.Take(5) : null; - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(Cipher); - await _deviceActionService.HideLoadingAsync(); - - if(saveTask.Succeeded) - { - _deviceActionService.Toast(AppResources.ItemUpdated); - _googleAnalyticsService.TrackAppEvent("EditedCipher"); - await Navigation.PopForDeviceAsync(); - } - else if(saveTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - - ToolbarItems.Add(saveToolBarItem); - } - - protected override void OnAppearing() - { - base.OnAppearing(); - NameCell?.InitEvents(); - NotesCell?.InitEvents(); - FolderCell?.InitEvents(); - - if(AttachmentsCell != null) - { - AttachmentsCell.Tapped += AttachmentsCell_Tapped; - } - if(DeleteCell != null) - { - DeleteCell.Tapped += DeleteCell_Tapped; - } - if(AddFieldCell != null) - { - AddFieldCell.Tapped += AddFieldCell_Tapped; - } - if(AddUriCell != null) - { - AddUriCell.Tapped += AddUriCell_Tapped; - } - - switch(Cipher.Type) - { - case CipherType.Login: - LoginPasswordCell?.InitEvents(); - LoginUsernameCell?.InitEvents(); - LoginTotpCell?.InitEvents(); - if(LoginPasswordCell?.Button1 != null) - { - LoginPasswordCell.Button1.Clicked += PasswordButton_Clicked; - } - if(LoginPasswordCell?.Button2 != null) - { - LoginPasswordCell.Button2.Clicked += PasswordButton2_Clicked; - } - if(LoginTotpCell?.Button1 != null) - { - LoginTotpCell.Button1.Clicked += TotpButton_Clicked; - } - break; - case CipherType.Card: - CardBrandCell?.InitEvents(); - CardCodeCell?.InitEvents(); - CardExpMonthCell?.InitEvents(); - CardExpYearCell?.InitEvents(); - CardNameCell?.InitEvents(); - CardNumberCell?.InitEvents(); - if(CardCodeCell?.Button1 != null) - { - CardCodeCell.Button1.Clicked += CardCodeButton_Clicked; - } - break; - case CipherType.Identity: - IdTitleCell?.InitEvents(); - IdFirstNameCell?.InitEvents(); - IdMiddleNameCell?.InitEvents(); - IdLastNameCell?.InitEvents(); - IdUsernameCell?.InitEvents(); - IdCompanyCell?.InitEvents(); - IdSsnCell?.InitEvents(); - IdPassportNumberCell?.InitEvents(); - IdLicenseNumberCell?.InitEvents(); - IdEmailCell?.InitEvents(); - IdPhoneCell?.InitEvents(); - IdAddress1Cell?.InitEvents(); - IdAddress2Cell?.InitEvents(); - IdAddress3Cell?.InitEvents(); - IdCityCell?.InitEvents(); - IdStateCell?.InitEvents(); - IdPostalCodeCell?.InitEvents(); - IdCountryCell?.InitEvents(); - break; - default: - break; - } - - Helpers.InitSectionEvents(FieldsSection); - Helpers.InitSectionEvents(UrisSection); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - - NameCell?.Dispose(); - NotesCell?.Dispose(); - FolderCell?.Dispose(); - - if(AttachmentsCell != null) - { - AttachmentsCell.Tapped -= AttachmentsCell_Tapped; - } - if(DeleteCell != null) - { - DeleteCell.Tapped -= DeleteCell_Tapped; - } - if(AddFieldCell != null) - { - AddFieldCell.Tapped -= AddFieldCell_Tapped; - } - if(AddUriCell != null) - { - AddUriCell.Tapped -= AddUriCell_Tapped; - } - - switch(Cipher.Type) - { - case CipherType.Login: - LoginTotpCell?.Dispose(); - LoginPasswordCell?.Dispose(); - LoginUsernameCell?.Dispose(); - if(LoginPasswordCell?.Button1 != null) - { - LoginPasswordCell.Button1.Clicked -= PasswordButton_Clicked; - } - if(LoginPasswordCell?.Button2 != null) - { - LoginPasswordCell.Button2.Clicked -= PasswordButton2_Clicked; - } - if(LoginTotpCell?.Button1 != null) - { - LoginTotpCell.Button1.Clicked -= TotpButton_Clicked; - } - break; - case CipherType.Card: - CardBrandCell?.Dispose(); - CardCodeCell?.Dispose(); - CardExpMonthCell?.Dispose(); - CardExpYearCell?.Dispose(); - CardNameCell?.Dispose(); - CardNumberCell?.Dispose(); - if(CardCodeCell?.Button1 != null) - { - CardCodeCell.Button1.Clicked -= CardCodeButton_Clicked; - } - break; - case CipherType.Identity: - IdTitleCell?.Dispose(); - IdFirstNameCell?.Dispose(); - IdMiddleNameCell?.Dispose(); - IdLastNameCell?.Dispose(); - IdUsernameCell?.Dispose(); - IdCompanyCell?.Dispose(); - IdSsnCell?.Dispose(); - IdPassportNumberCell?.Dispose(); - IdLicenseNumberCell?.Dispose(); - IdEmailCell?.Dispose(); - IdPhoneCell?.Dispose(); - IdAddress1Cell?.Dispose(); - IdAddress2Cell?.Dispose(); - IdAddress3Cell?.Dispose(); - IdCityCell?.Dispose(); - IdStateCell?.Dispose(); - IdPostalCodeCell?.Dispose(); - IdCountryCell?.Dispose(); - break; - default: - break; - } - - Helpers.DisposeSectionEvents(FieldsSection); - Helpers.DisposeSectionEvents(UrisSection); - } - - private void PasswordButton_Clicked(object sender, EventArgs e) - { - LoginPasswordCell.Entry.InvokeToggleIsPassword(); - LoginPasswordCell.Button1.Image = - "eye" + (!LoginPasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - } - - private async void PasswordButton2_Clicked(object sender, EventArgs e) - { - if(!string.IsNullOrWhiteSpace(LoginPasswordCell.Entry.Text) - && !(await DisplayAlert(null, AppResources.PasswordOverrideAlert, AppResources.Yes, AppResources.No))) - { - return; - } - - var page = new ToolsPasswordGeneratorPage((password) => - { - LoginPasswordCell.Entry.Text = password; - _deviceActionService.Toast(AppResources.PasswordGenerated); - }); - await Navigation.PushForDeviceAsync(page); - } - - private async void TotpButton_Clicked(object sender, EventArgs e) - { - var scanPage = new ScanPage((key) => - { - Device.BeginInvokeOnMainThread(async () => - { - await Navigation.PopModalAsync(); - if(!string.IsNullOrWhiteSpace(key)) - { - LoginTotpCell.Entry.Text = key; - _deviceActionService.Toast(AppResources.AuthenticatorKeyAdded); - } - else - { - await DisplayAlert(null, AppResources.AuthenticatorKeyReadError, AppResources.Ok); - } - }); - }); - - await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage)); - } - - private void CardCodeButton_Clicked(object sender, EventArgs e) - { - CardCodeCell.Entry.InvokeToggleIsPassword(); - CardCodeCell.Button1.Image = - "eye" + (!CardCodeCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - } - - private async void AttachmentsCell_Tapped(object sender, EventArgs e) - { - var page = new ExtendedNavigationPage(new VaultAttachmentsPage(_cipherId)); - await Navigation.PushModalAsync(page); - } - - private async void DeleteCell_Tapped(object sender, EventArgs e) - { - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - var confirmed = await DisplayAlert(null, AppResources.DoYouReallyWantToDelete, AppResources.Yes, - AppResources.No); - if(!confirmed) - { - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); - var deleteTask = await _cipherService.DeleteAsync(_cipherId); - await _deviceActionService.HideLoadingAsync(); - - if(deleteTask.Succeeded) - { - _deviceActionService.Toast(AppResources.ItemDeleted); - _googleAnalyticsService.TrackAppEvent("DeletedCipher"); - await Navigation.PopForDeviceAsync(); - } - else if(deleteTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, deleteTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - } - - private async void AddFieldCell_Tapped(object sender, EventArgs e) - { - await Helpers.AddField(this, FieldsSection); - } - - private void AddUriCell_Tapped(object sender, EventArgs e) - { - var cell = Helpers.MakeUriCell(string.Empty, null, UrisSection, this); - if(cell != null) - { - UrisSection.Insert(UrisSection.Count - 1, cell); - cell.InitEvents(); - } - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - } -} diff --git a/src/App/Pages/Vault/VaultListCiphersPage.cs b/src/App/Pages/Vault/VaultListCiphersPage.cs deleted file mode 100644 index 9bafd35c0..000000000 --- a/src/App/Pages/Vault/VaultListCiphersPage.cs +++ /dev/null @@ -1,547 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using Plugin.Settings.Abstractions; -using Plugin.Connectivity.Abstractions; -using System.Threading; -using static Bit.App.Models.Page.VaultListPageModel; -using System.Collections.Generic; - -namespace Bit.App.Pages -{ - public class VaultListCiphersPage : ExtendedContentPage - { - private readonly ICipherService _cipherService; - private readonly IConnectivity _connectivity; - private readonly ISyncService _syncService; - private readonly IDeviceInfoService _deviceInfoService; - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettingsService; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly IDeviceActionService _deviceActionService; - private readonly IFolderService _folderService; - private readonly ICollectionService _collectionService; - private CancellationTokenSource _filterResultsCancellationTokenSource; - private readonly bool _favorites = false; - private readonly bool _folder = false; - private readonly string _folderId = null; - private readonly string _collectionId = null; - private readonly string _groupingName = null; - private readonly string _uri = null; - - public VaultListCiphersPage(bool folder = false, string folderId = null, - string collectionId = null, string groupingName = null, bool favorites = false, string uri = null) - : base(true) - { - _folder = folder; - _folderId = folderId; - _collectionId = collectionId; - _favorites = favorites; - _groupingName = groupingName; - _uri = uri; - - _cipherService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _syncService = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _folderService = Resolver.Resolve(); - _collectionService = Resolver.Resolve(); - - Init(); - } - - public ExtendedObservableCollection> PresentationSections { get; private set; } - = new ExtendedObservableCollection>(); - public Cipher[] Ciphers { get; set; } = new Cipher[] { }; - public GroupingOrCipher[] Groupings { get; set; } = new GroupingOrCipher[] { }; - public ExtendedListView ListView { get; set; } - public SearchBar Search { get; set; } - public ActivityIndicator LoadingIndicator { get; set; } - public StackLayout NoDataStackLayout { get; set; } - public StackLayout ResultsStackLayout { get; set; } - private AddCipherToolBarItem AddCipherItem { get; set; } - public ContentView ContentView { get; set; } - public Fab Fab { get; set; } - - private void Init() - { - ListView = new ExtendedListView(ListViewCachingStrategy.RecycleElement) - { - IsGroupingEnabled = true, - ItemsSource = PresentationSections, - HasUnevenRows = true, - GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( - nameof(Section.Name), nameof(Section.Count))), - GroupShortNameBinding = new Binding(nameof(Section.NameShort)), - ItemTemplate = new GroupingOrCipherDataTemplateSelector(this) - }; - - if(Device.RuntimePlatform == Device.iOS) - { - ListView.RowHeight = -1; - } - - Search = new SearchBar - { - Placeholder = AppResources.Search, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Button)), - CancelButtonColor = Color.FromHex("3c8dbc") - }; - // Bug with search bar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975 - if(Device.RuntimePlatform == Device.Android && _deviceInfoService.Version >= 24) - { - Search.HeightRequest = 50; - } - - var noDataLabel = new Label - { - Text = _favorites ? AppResources.NoFavorites : AppResources.NoItems, - HorizontalTextAlignment = TextAlignment.Center, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"] - }; - - if(_folder || !string.IsNullOrWhiteSpace(_folderId)) - { - noDataLabel.Text = AppResources.NoItemsFolder; - } - else if(!string.IsNullOrWhiteSpace(_collectionId)) - { - noDataLabel.Text = AppResources.NoItemsCollection; - } - - NoDataStackLayout = new StackLayout - { - Children = { noDataLabel }, - VerticalOptions = LayoutOptions.CenterAndExpand, - Padding = new Thickness(20, 0), - Spacing = 20 - }; - - if(string.IsNullOrWhiteSpace(_collectionId) && !_favorites) - { - NoDataStackLayout.Children.Add(new ExtendedButton - { - Text = AppResources.AddAnItem, - Command = new Command(() => Helpers.AddCipher(this, _folderId)), - Style = (Style)Application.Current.Resources["btn-primaryAccent"] - }); - } - - ResultsStackLayout = new StackLayout - { - Children = { Search, ListView }, - Spacing = 0 - }; - - if(!string.IsNullOrWhiteSpace(_groupingName)) - { - Title = _groupingName; - } - else if(_favorites) - { - Title = AppResources.Favorites; - } - else - { - Title = AppResources.SearchVault; - - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.UWP) - { - ToolbarItems.Add(new DismissModalToolBarItem(this)); - } - } - - LoadingIndicator = new ActivityIndicator - { - IsRunning = true - }; - - if(Device.RuntimePlatform != Device.UWP) - { - LoadingIndicator.VerticalOptions = LayoutOptions.CenterAndExpand; - LoadingIndicator.HorizontalOptions = LayoutOptions.Center; - } - - ContentView = new ContentView - { - Content = LoadingIndicator - }; - - var fabLayout = new FabLayout(ContentView); - if(!string.IsNullOrWhiteSpace(_uri) || _folder || !string.IsNullOrWhiteSpace(_folderId)) - { - if(Device.RuntimePlatform == Device.Android) - { - ListView.BottomPadding = 170; - Fab = new Fab(fabLayout, "plus.png", (sender, args) => Helpers.AddCipher(this, _folderId)); - } - else - { - AddCipherItem = new AddCipherToolBarItem(this, _folderId); - ToolbarItems.Add(AddCipherItem); - } - } - else if(Device.RuntimePlatform == Device.Android) - { - ListView.BottomPadding = 50; - } - - Content = fabLayout; - } - - private void SearchBar_SearchButtonPressed(object sender, EventArgs e) - { - _filterResultsCancellationTokenSource = FilterResultsBackground(((SearchBar)sender).Text, - _filterResultsCancellationTokenSource); - } - - private void SearchBar_TextChanged(object sender, TextChangedEventArgs e) - { - var oldLength = e.OldTextValue?.Length ?? 0; - var newLength = e.NewTextValue?.Length ?? 0; - if(oldLength < 2 && newLength < 2 && oldLength < newLength) - { - return; - } - - _filterResultsCancellationTokenSource = FilterResultsBackground(e.NewTextValue, - _filterResultsCancellationTokenSource); - } - - private CancellationTokenSource FilterResultsBackground(string searchFilter, - CancellationTokenSource previousCts) - { - var cts = new CancellationTokenSource(); - Task.Run(async () => - { - if(!string.IsNullOrWhiteSpace(searchFilter)) - { - await Task.Delay(300); - if(searchFilter != Search.Text) - { - return; - } - else - { - previousCts?.Cancel(); - } - } - - try - { - FilterResults(searchFilter, cts.Token); - } - catch(OperationCanceledException) { } - }, cts.Token); - - return cts; - } - - private void FilterResults(string searchFilter, CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - if(string.IsNullOrWhiteSpace(searchFilter)) - { - LoadSections(Ciphers, Groupings, ct); - } - else - { - searchFilter = searchFilter.ToLower(); - var filteredCiphers = Ciphers - .Where(s => s.Name.ToLower().Contains(searchFilter) || - (s.Subtitle?.ToLower().Contains(searchFilter) ?? false) || - (s.LoginUri?.ToLower().Contains(searchFilter) ?? false)) - .TakeWhile(s => !ct.IsCancellationRequested) - .ToArray(); - - ct.ThrowIfCancellationRequested(); - LoadSections(filteredCiphers, null, ct); - } - } - - protected override bool OnBackButtonPressed() - { - if(string.IsNullOrWhiteSpace(_uri)) - { - return false; - } - - _googleAnalyticsService.TrackExtensionEvent("BackClosed", _uri.StartsWith("http") ? "Website" : "App"); - _deviceActionService.CloseAutofill(); - return true; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - MessagingCenter.Subscribe(Application.Current, "SyncCompleted", (sender, success) => - { - if(success) - { - _filterResultsCancellationTokenSource = FetchAndLoadVault(); - } - }); - - AddCipherItem?.InitEvents(); - ListView.ItemSelected += GroupingOrCipherSelected; - Search.TextChanged += SearchBar_TextChanged; - Search.SearchButtonPressed += SearchBar_SearchButtonPressed; - _filterResultsCancellationTokenSource = FetchAndLoadVault(); - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - MessagingCenter.Unsubscribe(Application.Current, "SyncCompleted"); - - AddCipherItem?.Dispose(); - ListView.ItemSelected -= GroupingOrCipherSelected; - Search.TextChanged -= SearchBar_TextChanged; - Search.SearchButtonPressed -= SearchBar_SearchButtonPressed; - } - - private CancellationTokenSource FetchAndLoadVault() - { - var cts = new CancellationTokenSource(); - if(PresentationSections.Count > 0 && _syncService.SyncInProgress) - { - return cts; - } - - _filterResultsCancellationTokenSource?.Cancel(); - - Task.Run(async () => - { - IEnumerable ciphers; - if(_folder || !string.IsNullOrWhiteSpace(_folderId)) - { - ciphers = await _cipherService.GetAllByFolderAsync(_folderId); - if(!string.IsNullOrWhiteSpace(_folderId)) - { - var folders = await _folderService.GetAllAsync(); - var fGroupings = folders.Select(f => new Grouping(f, null)).OrderBy(g => g.Name).ToList(); - var fTreeNodes = Helpers.GetAllNested(fGroupings); - var fTreeNode = Helpers.GetTreeNodeObject(fTreeNodes, _folderId); - if(fTreeNode.Children?.Any() ?? false) - { - Groupings = fTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray(); - } - } - } - else if(!string.IsNullOrWhiteSpace(_collectionId)) - { - ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId); - - var collections = await _collectionService.GetAllAsync(); - var cGroupings = collections.Select(c => new Grouping(c, null)).OrderBy(g => g.Name).ToList(); - var cTreeNodes = Helpers.GetAllNested(cGroupings); - var cTreeNode = Helpers.GetTreeNodeObject(cTreeNodes, _collectionId); - if(cTreeNode.Children?.Any() ?? false) - { - Groupings = cTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray(); - } - } - else if(_favorites) - { - ciphers = await _cipherService.GetAllAsync(true); - } - else - { - ciphers = await _cipherService.GetAllAsync(); - } - - Ciphers = ciphers - .Select(s => new Cipher(s, _appSettingsService)) - .OrderBy(s => - { - if(string.IsNullOrWhiteSpace(s.Name)) - { - return 2; - } - - return s.Name.Length > 0 && Char.IsDigit(s.Name[0]) ? 0 : (Char.IsLetter(s.Name[0]) ? 1 : 2); - }) - .ThenBy(s => s.Name) - .ThenBy(s => s.Subtitle) - .ToArray(); - - try - { - FilterResults(Search.Text, cts.Token); - } - catch(OperationCanceledException) { } - }, cts.Token); - - return cts; - } - - private void LoadSections(Cipher[] ciphers, GroupingOrCipher[] groupings, CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - var sections = ciphers.GroupBy(c => c.NameGroup.ToUpperInvariant()) - .Select(g => new Section(g.Select(g2 => new GroupingOrCipher(g2)).ToList(), g.Key)) - .ToList(); - - if(groupings?.Any() ?? false) - { - sections.Insert(0, new Section(groupings.ToList(), - _folder ? AppResources.Folders : AppResources.Collections)); - } - - ct.ThrowIfCancellationRequested(); - Device.BeginInvokeOnMainThread(() => - { - PresentationSections.ResetWithRange(sections); - if(PresentationSections.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text)) - { - ContentView.Content = ResultsStackLayout; - - if(string.IsNullOrWhiteSpace(_uri) && !_folder && string.IsNullOrWhiteSpace(_folderId) && - string.IsNullOrWhiteSpace(_collectionId) && !_favorites) - { - Search.Focus(); - } - } - else if(_syncService.SyncInProgress) - { - ContentView.Content = LoadingIndicator; - } - else - { - ContentView.Content = NoDataStackLayout; - } - }); - } - - private async void GroupingOrCipherSelected(object sender, SelectedItemChangedEventArgs e) - { - var groupingOrCipher = e.SelectedItem as GroupingOrCipher; - if(groupingOrCipher == null) - { - return; - } - - if(groupingOrCipher.Grouping != null) - { - Page page; - if(groupingOrCipher.Grouping.Node.Folder) - { - page = new VaultListCiphersPage(folder: true, - folderId: groupingOrCipher.Grouping.Node.Id, groupingName: groupingOrCipher.Grouping.Node.Name); - } - else - { - page = new VaultListCiphersPage(collectionId: groupingOrCipher.Grouping.Node.Id, - groupingName: groupingOrCipher.Grouping.Node.Name); - } - - await Navigation.PushAsync(page); - } - else if(groupingOrCipher.Cipher != null) - { - var cipher = groupingOrCipher.Cipher; - string selection = null; - if(!string.IsNullOrWhiteSpace(_uri)) - { - var options = new List { AppResources.Autofill }; - if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected) - { - options.Add(AppResources.AutofillAndSave); - } - options.Add(AppResources.View); - selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null, - options.ToArray()); - } - - if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri)) - { - var page = new VaultViewCipherPage(cipher.Type, cipher.Id); - await Navigation.PushForDeviceAsync(page); - } - else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) - { - if(selection == AppResources.AutofillAndSave) - { - if(!_connectivity.IsConnected) - { - Helpers.AlertNoConnection(this); - } - else - { - var uris = cipher.CipherModel.Login?.Uris?.ToList(); - if(uris == null) - { - uris = new List(); - } - - uris.Add(new Models.LoginUri - { - Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId), - Match = null - }); - - cipher.CipherModel.Login.Uris = uris; - - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); - await _deviceActionService.HideLoadingAsync(); - if(saveTask.Succeeded) - { - _googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); - } - } - } - - if(_deviceInfoService.Version < 21) - { - Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri)); - } - else - { - _googleAnalyticsService.TrackExtensionEvent("AutoFilled", - _uri.StartsWith("http") ? "Website" : "App"); - _deviceActionService.Autofill(cipher); - } - } - } - - ((ListView)sender).SelectedItem = null; - } - - public class GroupingOrCipherDataTemplateSelector : DataTemplateSelector - { - public GroupingOrCipherDataTemplateSelector(VaultListCiphersPage page) - { - GroupingTemplate = new DataTemplate(() => new VaultGroupingViewCell()); - CipherTemplate = new DataTemplate(() => new VaultListViewCell( - (Cipher c) => Helpers.CipherMoreClickedAsync(page, c, !string.IsNullOrWhiteSpace(page._uri)), - true)); - } - - public DataTemplate GroupingTemplate { get; set; } - public DataTemplate CipherTemplate { get; set; } - - protected override DataTemplate OnSelectTemplate(object item, BindableObject container) - { - if(item == null) - { - return null; - } - return ((GroupingOrCipher)item).Cipher == null ? GroupingTemplate : CipherTemplate; - } - } - } -} diff --git a/src/App/Pages/Vault/VaultListGroupingsPage.cs b/src/App/Pages/Vault/VaultListGroupingsPage.cs deleted file mode 100644 index 75ca19919..000000000 --- a/src/App/Pages/Vault/VaultListGroupingsPage.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using Plugin.Settings.Abstractions; -using Plugin.Connectivity.Abstractions; -using System.Collections.Generic; -using System.Threading; -using static Bit.App.Models.Page.VaultListPageModel; - -namespace Bit.App.Pages -{ - public class VaultListGroupingsPage : ExtendedContentPage - { - private readonly IFolderService _folderService; - private readonly ICollectionService _collectionService; - private readonly ICipherService _cipherService; - private readonly IConnectivity _connectivity; - private readonly IDeviceActionService _deviceActionService; - private readonly ISyncService _syncService; - private readonly IPushNotificationService _pushNotification; - private readonly IDeviceInfoService _deviceInfoService; - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettingsService; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private CancellationTokenSource _filterResultsCancellationTokenSource; - - public VaultListGroupingsPage() - : base(true) - { - _folderService = Resolver.Resolve(); - _collectionService = Resolver.Resolve(); - _cipherService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _syncService = Resolver.Resolve(); - _pushNotification = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _appSettingsService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public ExtendedObservableCollection> PresentationSections { get; private set; } - = new ExtendedObservableCollection>(); - public ExtendedListView ListView { get; set; } - public StackLayout NoDataStackLayout { get; set; } - public ActivityIndicator LoadingIndicator { get; set; } - private AddCipherToolBarItem AddCipherItem { get; set; } - private SearchToolBarItem SearchItem { get; set; } - public ContentView ContentView { get; set; } - public Fab Fab { get; set; } - - private void Init() - { - SearchItem = new SearchToolBarItem(this); - ToolbarItems.Add(SearchItem); - - ListView = new ExtendedListView(ListViewCachingStrategy.RecycleElement) - { - IsGroupingEnabled = true, - ItemsSource = PresentationSections, - HasUnevenRows = true, - GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( - nameof(Section.Name), nameof(Section.Count), new Thickness(16, 12))), - ItemTemplate = new GroupingOrCipherDataTemplateSelector(this) - }; - - if(Device.RuntimePlatform == Device.iOS) - { - ListView.RowHeight = -1; - } - - var noDataLabel = new Label - { - Text = AppResources.NoItems, - HorizontalTextAlignment = TextAlignment.Center, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"] - }; - - var addCipherButton = new ExtendedButton - { - Text = AppResources.AddAnItem, - Command = new Command(() => Helpers.AddCipher(this, null)), - Style = (Style)Application.Current.Resources["btn-primaryAccent"] - }; - - NoDataStackLayout = new StackLayout - { - Children = { noDataLabel, addCipherButton }, - VerticalOptions = LayoutOptions.CenterAndExpand, - Padding = new Thickness(20, 0), - Spacing = 20 - }; - - LoadingIndicator = new ActivityIndicator - { - IsRunning = true - }; - - if(Device.RuntimePlatform != Device.UWP) - { - LoadingIndicator.VerticalOptions = LayoutOptions.CenterAndExpand; - LoadingIndicator.HorizontalOptions = LayoutOptions.Center; - } - - ContentView = new ContentView - { - Content = LoadingIndicator - }; - - var fabLayout = new FabLayout(ContentView); - if(Device.RuntimePlatform == Device.Android) - { - Fab = new Fab(fabLayout, "plus.png", (sender, args) => Helpers.AddCipher(this, null)); - ListView.BottomPadding = 170; - } - else - { - AddCipherItem = new AddCipherToolBarItem(this, null); - ToolbarItems.Add(AddCipherItem); - } - - Content = fabLayout; - Title = AppResources.MyVault; - } - - protected async override void OnAppearing() - { - base.OnAppearing(); - MessagingCenter.Subscribe(Application.Current, "SyncCompleted", (sender, success) => - { - if(success) - { - _filterResultsCancellationTokenSource = FetchAndLoadVault(); - } - }); - - ListView.ItemSelected += GroupingOrCipherSelected; - AddCipherItem?.InitEvents(); - SearchItem?.InitEvents(); - - _filterResultsCancellationTokenSource = FetchAndLoadVault(); - - // Push registration - if(_connectivity.IsConnected) - { - var lastPushRegistration = _settings.GetValueOrDefault(Constants.PushLastRegistrationDate, - DateTime.MinValue); - - if(Device.RuntimePlatform == Device.iOS) - { - var pushPromptShow = _settings.GetValueOrDefault(Constants.PushInitialPromptShown, false); - if(!pushPromptShow) - { - _settings.AddOrUpdateValue(Constants.PushInitialPromptShown, true); - await DisplayAlert(AppResources.EnableAutomaticSyncing, AppResources.PushNotificationAlert, - AppResources.OkGotIt); - } - - if(!pushPromptShow || DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1)) - { - _pushNotification.Register(); - } - } - else if(Device.RuntimePlatform == Device.Android && - DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1)) - { - _pushNotification.Register(); - } - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - MessagingCenter.Unsubscribe(Application.Current, "SyncCompleted"); - - ListView.ItemSelected -= GroupingOrCipherSelected; - AddCipherItem?.Dispose(); - SearchItem?.Dispose(); - } - - private CancellationTokenSource FetchAndLoadVault() - { - var cts = new CancellationTokenSource(); - _filterResultsCancellationTokenSource?.Cancel(); - - Task.Run(async () => - { - var sections = new List>(); - var favoriteCipherGroupings = new List(); - var noFolderCipherGroupings = new List(); - var ciphers = await _cipherService.GetAllAsync(); - var collectionsDict = (await _collectionService.GetAllCipherAssociationsAsync()) - .GroupBy(c => c.Item2).ToDictionary(g => g.Key, v => v.ToList()); - - var folderCounts = new Dictionary(); - foreach(var cipher in ciphers) - { - if(cipher.Favorite) - { - favoriteCipherGroupings.Add(new GroupingOrCipher(new Cipher(cipher, _appSettingsService))); - } - - if(cipher.FolderId != null) - { - if(!folderCounts.ContainsKey(cipher.FolderId)) - { - folderCounts.Add(cipher.FolderId, 0); - } - folderCounts[cipher.FolderId]++; - } - else - { - noFolderCipherGroupings.Add(new GroupingOrCipher(new Cipher(cipher, _appSettingsService))); - } - } - - if(favoriteCipherGroupings.Any()) - { - sections.Add(new Section( - favoriteCipherGroupings.OrderBy(g => g.Cipher.Name).ThenBy(g => g.Cipher.Subtitle).ToList(), - AppResources.Favorites)); - } - - var folders = await _folderService.GetAllAsync(); - var collections = await _collectionService.GetAllAsync(); - - var fGroupings = folders - .Select(f => new Grouping(f, folderCounts.ContainsKey(f.Id) ? folderCounts[f.Id] : 0)) - .OrderBy(g => g.Name); - var folderGroupings = Helpers.GetAllNested(fGroupings) - .Select(n => new GroupingOrCipher(n)).ToList(); - - if(collections.Any() || noFolderCipherGroupings.Count >= 100) - { - var noneFolderGrouping = new Grouping(AppResources.FolderNone, noFolderCipherGroupings.Count); - var noneFolderNode = new Bit.App.Models.TreeNode(noneFolderGrouping, - noneFolderGrouping.Name, null); - folderGroupings.Add(new GroupingOrCipher(noneFolderNode)); - } - - if(folderGroupings.Any()) - { - sections.Add(new Section(folderGroupings, AppResources.Folders)); - } - - var cGroupings = collections - .Select(c => new Grouping(c, collectionsDict.ContainsKey(c.Id) ? collectionsDict[c.Id].Count() : 0)) - .OrderBy(g => g.Name); - var collectionGroupings = Helpers.GetAllNested(cGroupings) - .Select(n => new GroupingOrCipher(n)).ToList(); - - if(collectionGroupings.Any()) - { - sections.Add(new Section(collectionGroupings, AppResources.Collections)); - } - else if(noFolderCipherGroupings.Count > 0 && noFolderCipherGroupings.Count < 100) - { - sections.Add(new Section( - noFolderCipherGroupings.OrderBy(g => g.Cipher.Name).ThenBy(g => g.Cipher.Subtitle).ToList(), - AppResources.FolderNone)); - } - - Device.BeginInvokeOnMainThread(() => - { - PresentationSections.ResetWithRange(sections); - - if(ciphers.Any() || folders.Any()) - { - ContentView.Content = ListView; - } - else if(_syncService.SyncInProgress) - { - ContentView.Content = LoadingIndicator; - } - else - { - ContentView.Content = NoDataStackLayout; - } - }); - }, cts.Token); - - return cts; - } - - private async void GroupingOrCipherSelected(object sender, SelectedItemChangedEventArgs e) - { - var groupingOrCipher = e.SelectedItem as GroupingOrCipher; - if(groupingOrCipher == null) - { - return; - } - - if(groupingOrCipher.Grouping != null) - { - Page page; - if(groupingOrCipher.Grouping.Node.Folder) - { - page = new VaultListCiphersPage(folder: true, - folderId: groupingOrCipher.Grouping.Node.Id, groupingName: groupingOrCipher.Grouping.Node.Name); - } - else - { - page = new VaultListCiphersPage(collectionId: groupingOrCipher.Grouping.Node.Id, - groupingName: groupingOrCipher.Grouping.Node.Name); - } - - await Navigation.PushAsync(page); - } - else if(groupingOrCipher.Cipher != null) - { - var page = new VaultViewCipherPage(groupingOrCipher.Cipher.Type, groupingOrCipher.Cipher.Id); - await Navigation.PushForDeviceAsync(page); - } - - ((ListView)sender).SelectedItem = null; - } - - private async void Search() - { - var page = new ExtendedNavigationPage(new VaultListCiphersPage()); - await Navigation.PushModalAsync(page); - } - - private class SearchToolBarItem : ExtendedToolbarItem - { - public SearchToolBarItem(VaultListGroupingsPage page) - : base(() => page.Search()) - { - Text = AppResources.Search; - Icon = "search.png"; - } - } - - public class GroupingOrCipherDataTemplateSelector : DataTemplateSelector - { - public GroupingOrCipherDataTemplateSelector(VaultListGroupingsPage page) - { - GroupingTemplate = new DataTemplate(() => new VaultGroupingViewCell()); - CipherTemplate = new DataTemplate(() => new VaultListViewCell( - (Cipher c) => Helpers.CipherMoreClickedAsync(page, c, false), true)); - } - - public DataTemplate GroupingTemplate { get; set; } - public DataTemplate CipherTemplate { get; set; } - - protected override DataTemplate OnSelectTemplate(object item, BindableObject container) - { - if(item == null) - { - return null; - } - return ((GroupingOrCipher)item).Cipher == null ? GroupingTemplate : CipherTemplate; - } - } - } -} diff --git a/src/App/Pages/Vault/VaultViewCipherPage.cs b/src/App/Pages/Vault/VaultViewCipherPage.cs deleted file mode 100644 index eb8fd3769..000000000 --- a/src/App/Pages/Vault/VaultViewCipherPage.cs +++ /dev/null @@ -1,666 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Bit.App.Utilities; -using System.Collections.Generic; -using Bit.App.Models; -using System.Linq; -using Bit.App.Enums; - -namespace Bit.App.Pages -{ - public class VaultViewCipherPage : ExtendedContentPage - { - private readonly CipherType _type; - private readonly string _cipherId; - private readonly ICipherService _cipherService; - private readonly IDeviceActionService _deviceActionService; - private DateTime? _timerStarted = null; - private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(5); - - public VaultViewCipherPage(CipherType type, string cipherId) - { - _type = type; - _cipherId = cipherId; - _cipherService = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - - Init(); - } - - public Fab Fab { get; set; } - private VaultViewCipherPageModel Model { get; set; } = new VaultViewCipherPageModel(); - private ExtendedTableView Table { get; set; } - private TableSection ItemInformationSection { get; set; } - public TableSection UrisSection { get; set; } - private TableSection NotesSection { get; set; } - private TableSection AttachmentsSection { get; set; } - private TableSection FieldsSection { get; set; } - public TableSection OtherSection { get; set; } - public LabeledValueCell NotesCell { get; set; } - private EditCipherToolBarItem EditItem { get; set; } - public List FieldsCells { get; set; } - public List AttachmentCells { get; set; } - - // Login - public LabeledValueCell LoginUsernameCell { get; set; } - public LabeledValueCell LoginPasswordCell { get; set; } - public LabeledValueCell LoginPasswordRevisionDateCell { get; set; } - public LabeledValueCell LoginTotpCodeCell { get; set; } - - // Card - public LabeledValueCell CardNameCell { get; set; } - public LabeledValueCell CardNumberCell { get; set; } - public LabeledValueCell CardBrandCell { get; set; } - public LabeledValueCell CardExpCell { get; set; } - public LabeledValueCell CardCodeCell { get; set; } - - // Card - public LabeledValueCell IdNameCell { get; set; } - public LabeledValueCell IdUsernameCell { get; set; } - public LabeledValueCell IdCompanyCell { get; set; } - public LabeledValueCell IdSsnCell { get; set; } - public LabeledValueCell IdPassportNumberCell { get; set; } - public LabeledValueCell IdLicenseNumberCell { get; set; } - public LabeledValueCell IdEmailCell { get; set; } - public LabeledValueCell IdPhoneCell { get; set; } - public LabeledValueCell IdAddressCell { get; set; } - - private void Init() - { - if(Device.RuntimePlatform == Device.iOS) - { - ToolbarItems.Add(new DismissModalToolBarItem(this)); - } - - InitProps(); - - var fabLayout = new FabLayout(Table); - if(Device.RuntimePlatform == Device.Android) - { - Fab = new Fab(fabLayout, "pencil.png", async (sender, args) => - { - await Navigation.PushForDeviceAsync(new VaultEditCipherPage(_cipherId)); - }); - } - else - { - EditItem = new EditCipherToolBarItem(this, _cipherId); - ToolbarItems.Add(EditItem); - } - - Content = fabLayout; - Title = AppResources.ViewItem; - BindingContext = Model; - } - - public void InitProps() - { - // Name - var nameCell = new LabeledValueCell(AppResources.Name); - nameCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.Name)); - nameCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - // Notes - NotesCell = new LabeledValueCell(); - NotesCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.Notes)); - NotesCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - var revisionDateCell = new LabeledValueCell(AppResources.DateUpdated); - revisionDateCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.RevisionDate)); - revisionDateCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - switch(_type) - { - case CipherType.Login: - // Username - LoginUsernameCell = new LabeledValueCell(AppResources.Username, button1Image: "clipboard.png"); - LoginUsernameCell.Value.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.LoginUsername)); - LoginUsernameCell.Button1.Command = - new Command(() => Copy(Model.LoginUsername, AppResources.Username)); - LoginUsernameCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - // Password - LoginPasswordCell = new LabeledValueCell(AppResources.Password, button1Image: string.Empty, - button2Image: "clipboard.png"); - LoginPasswordCell.Value.SetBinding(Label.FormattedTextProperty, - nameof(VaultViewCipherPageModel.FormattedLoginPassword)); - LoginPasswordCell.Button1.SetBinding(Button.ImageProperty, - nameof(VaultViewCipherPageModel.LoginShowHideImage)); - LoginPasswordCell.Button1.Command = - new Command(() => Model.RevealLoginPassword = !Model.RevealLoginPassword); - LoginPasswordCell.Button2.Command = - new Command(() => Copy(Model.LoginPassword, AppResources.Password)); - LoginPasswordCell.Value.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - LoginPasswordCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - // Totp - LoginTotpCodeCell = new LabeledValueCell( - AppResources.VerificationCodeTotp, button1Image: "clipboard.png", subText: "--"); - LoginTotpCodeCell.Value.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.LoginTotpCodeFormatted)); - LoginTotpCodeCell.Value.SetBinding(Label.TextColorProperty, - nameof(VaultViewCipherPageModel.LoginTotpColor)); - LoginTotpCodeCell.Button1.Command = - new Command(() => Copy(Model.LoginTotpCode, AppResources.VerificationCodeTotp)); - LoginTotpCodeCell.Sub.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.LoginTotpSecond)); - LoginTotpCodeCell.Sub.SetBinding(Label.TextColorProperty, - nameof(VaultViewCipherPageModel.LoginTotpColor)); - LoginTotpCodeCell.Value.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - - // Password Revision Date - LoginPasswordRevisionDateCell = new LabeledValueCell(AppResources.DatePasswordUpdated); - LoginPasswordRevisionDateCell.Value.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.PasswordRevisionDate)); - LoginPasswordRevisionDateCell.Value.LineBreakMode = LineBreakMode.WordWrap; - break; - case CipherType.Card: - CardNameCell = new LabeledValueCell(AppResources.CardholderName); - CardNameCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.CardName)); - - CardNumberCell = new LabeledValueCell(AppResources.Number, button1Image: "clipboard.png"); - CardNumberCell.Button1.Command = new Command(() => Copy(Model.CardNumber, AppResources.Number)); - CardNumberCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.CardNumber)); - CardNumberCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - CardBrandCell = new LabeledValueCell(AppResources.Brand); - CardBrandCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.CardBrand)); - - CardExpCell = new LabeledValueCell(AppResources.Expiration); - CardExpCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.CardExp)); - - CardCodeCell = new LabeledValueCell(AppResources.SecurityCode, button1Image: string.Empty, - button2Image: "clipboard.png"); - CardCodeCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.MaskedCardCode)); - CardCodeCell.Button1.SetBinding(Button.ImageProperty, - nameof(VaultViewCipherPageModel.CardCodeShowHideImage)); - CardCodeCell.Button1.Command = new Command(() => Model.RevealCardCode = !Model.RevealCardCode); - CardCodeCell.Button2.Command = new Command(() => Copy(Model.CardCode, AppResources.SecurityCode)); - CardCodeCell.Value.FontFamily = - Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - break; - case CipherType.Identity: - IdNameCell = new LabeledValueCell(AppResources.Name); - IdNameCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdName)); - IdNameCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - IdUsernameCell = new LabeledValueCell(AppResources.Username, button1Image: "clipboard.png"); - IdUsernameCell.Button1.Command = new Command(() => Copy(Model.IdUsername, AppResources.Username)); - IdUsernameCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdUsername)); - IdUsernameCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - IdCompanyCell = new LabeledValueCell(AppResources.Company); - IdCompanyCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdCompany)); - - IdSsnCell = new LabeledValueCell(AppResources.SSN); - IdSsnCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdSsn)); - - IdPassportNumberCell = new LabeledValueCell(AppResources.PassportNumber, - button1Image: "clipboard.png"); - IdPassportNumberCell.Button1.Command = - new Command(() => Copy(Model.IdPassportNumber, AppResources.PassportNumber)); - IdPassportNumberCell.Value.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.IdPassportNumber)); - IdPassportNumberCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - IdLicenseNumberCell = new LabeledValueCell(AppResources.LicenseNumber, - button1Image: "clipboard.png"); - IdLicenseNumberCell.Button1.Command = - new Command(() => Copy(Model.IdLicenseNumber, AppResources.LicenseNumber)); - IdLicenseNumberCell.Value.SetBinding(Label.TextProperty, - nameof(VaultViewCipherPageModel.IdLicenseNumber)); - IdLicenseNumberCell.Value.LineBreakMode = LineBreakMode.WordWrap; - - IdEmailCell = new LabeledValueCell(AppResources.Email); - IdEmailCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdEmail)); - - IdPhoneCell = new LabeledValueCell(AppResources.Phone); - IdPhoneCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdPhone)); - - IdAddressCell = new LabeledValueCell(AppResources.Address, button1Image: "clipboard.png"); - IdAddressCell.Button1.Command = new Command(() => Copy(Model.IdAddress, AppResources.Address)); - IdAddressCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewCipherPageModel.IdAddress)); - IdAddressCell.Value.LineBreakMode = LineBreakMode.WordWrap; - break; - default: - break; - } - - ItemInformationSection = new TableSection(AppResources.ItemInformation) - { - nameCell - }; - - NotesSection = new TableSection(AppResources.Notes) - { - NotesCell - }; - - OtherSection = new TableSection(Helpers.GetEmptyTableSectionTitle()) - { - revisionDateCell - }; - - Table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = true, - HasUnevenRows = true, - EnableSelection = true, - Root = new TableRoot - { - ItemInformationSection - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - Table.RowHeight = -1; - Table.EstimatedRowHeight = 70; - } - else if(Device.RuntimePlatform == Device.Android) - { - Table.BottomPadding = 170; - } - } - - protected async override void OnAppearing() - { - NotesCell.Tapped += NotesCell_Tapped; - EditItem?.InitEvents(); - - var cipher = await _cipherService.GetByIdAsync(_cipherId); - if(cipher == null) - { - await Navigation.PopForDeviceAsync(); - return; - } - - Model.Update(cipher); - BuildTable(cipher); - base.OnAppearing(); - } - - protected override void OnDisappearing() - { - _timerStarted = null; - NotesCell.Tapped -= NotesCell_Tapped; - EditItem?.Dispose(); - CleanupAttachmentCells(); - } - - private void BuildTable(Cipher cipher) - { - // URIs - if(UrisSection != null && Table.Root.Contains(UrisSection)) - { - Table.Root.Remove(UrisSection); - } - if(Model.ShowLoginUris) - { - UrisSection = new TableSection(Helpers.GetEmptyTableSectionTitle()); - foreach(var uri in Model.LoginUris) - { - UrisSection.Add(new UriViewCell(this, uri)); - } - Table.Root.Add(UrisSection); - } - - // Notes - if(Table.Root.Contains(NotesSection)) - { - Table.Root.Remove(NotesSection); - } - if(Model.ShowNotes) - { - Table.Root.Add(NotesSection); - } - - // Fields - if(Table.Root.Contains(FieldsSection)) - { - Table.Root.Remove(FieldsSection); - } - if(Model.ShowFields) - { - FieldsSection = new TableSection(AppResources.CustomFields); - foreach(var field in Model.Fields) - { - FieldViewCell fieldCell; - switch(field.Type) - { - case FieldType.Text: - fieldCell = new FieldViewCell(this, field, null); - break; - case FieldType.Hidden: - fieldCell = new FieldViewCell(this, field, null, null); - break; - case FieldType.Boolean: - fieldCell = new FieldViewCell(this, field); - break; - default: - continue; - } - FieldsSection.Add(fieldCell); - } - Table.Root.Add(FieldsSection); - } - - // Attachments - CleanupAttachmentCells(); - if(Table.Root.Contains(AttachmentsSection)) - { - Table.Root.Remove(AttachmentsSection); - } - if(Model.ShowAttachments && (Helpers.CanAccessPremium() || cipher.OrganizationId != null)) - { - AttachmentsSection = new TableSection(AppResources.Attachments); - AttachmentCells = new List(); - foreach(var attachment in Model.Attachments.OrderBy(s => s.Name)) - { - var attachmentCell = new AttachmentViewCell(attachment, async () => - { - await OpenAttachmentAsync(cipher, attachment); - }); - AttachmentCells.Add(attachmentCell); - AttachmentsSection.Add(attachmentCell); - attachmentCell.InitEvents(); - } - Table.Root.Add(AttachmentsSection); - } - - // Other - if(Table.Root.Contains(OtherSection)) - { - Table.Root.Remove(OtherSection); - } - Table.Root.Add(OtherSection); - - // Various types - switch(cipher.Type) - { - case CipherType.Login: - if(OtherSection.Contains(LoginPasswordRevisionDateCell)) - { - OtherSection.Remove(LoginPasswordRevisionDateCell); - } - if(Model.ShowPasswordRevisionDate) - { - OtherSection.Add(LoginPasswordRevisionDateCell); - } - - AddSectionCell(LoginUsernameCell, Model.ShowLoginUsername); - AddSectionCell(LoginPasswordCell, Model.ShowLoginPassword); - - if(ItemInformationSection.Contains(LoginTotpCodeCell)) - { - ItemInformationSection.Remove(LoginTotpCodeCell); - } - if(cipher.Login?.Totp != null && (Helpers.CanAccessPremium() || cipher.OrganizationUseTotp)) - { - var totpKey = cipher.Login?.Totp.Decrypt(cipher.OrganizationId); - if(!string.IsNullOrWhiteSpace(totpKey)) - { - var otpParams = new OtpAuth(totpKey); - Model.LoginTotpCode = Crypto.Totp(totpKey); - if(!string.IsNullOrWhiteSpace(Model.LoginTotpCode)) - { - TotpTick(totpKey, otpParams.Period); - _timerStarted = DateTime.Now; - Device.StartTimer(new TimeSpan(0, 0, 1), () => - { - if(_timerStarted == null || (DateTime.Now - _timerStarted) > _timerMaxLength) - { - return false; - } - - TotpTick(totpKey, otpParams.Period); - return true; - }); - - ItemInformationSection.Add(LoginTotpCodeCell); - } - } - } - break; - case CipherType.Card: - AddSectionCell(CardNameCell, Model.ShowCardName); - AddSectionCell(CardNumberCell, Model.ShowCardNumber); - AddSectionCell(CardBrandCell, Model.ShowCardBrand); - AddSectionCell(CardExpCell, Model.ShowCardExp); - AddSectionCell(CardCodeCell, Model.ShowCardCode); - break; - case CipherType.Identity: - AddSectionCell(IdNameCell, Model.ShowIdName); - AddSectionCell(IdUsernameCell, Model.ShowIdUsername); - AddSectionCell(IdCompanyCell, Model.ShowIdCompany); - AddSectionCell(IdSsnCell, Model.ShowIdSsn); - AddSectionCell(IdPassportNumberCell, Model.ShowIdPassportNumber); - AddSectionCell(IdLicenseNumberCell, Model.ShowIdLicenseNumber); - AddSectionCell(IdEmailCell, Model.ShowIdEmail); - AddSectionCell(IdPhoneCell, Model.ShowIdPhone); - AddSectionCell(IdAddressCell, Model.ShowIdAddress); - break; - default: - break; - } - } - - private void AddSectionCell(LabeledValueCell cell, bool show) - { - if(ItemInformationSection.Contains(cell)) - { - ItemInformationSection.Remove(cell); - } - if(show) - { - ItemInformationSection.Add(cell); - } - } - - private void CleanupAttachmentCells() - { - if(AttachmentCells != null) - { - foreach(var cell in AttachmentCells) - { - cell.Dispose(); - } - } - } - - private async Task OpenAttachmentAsync(Cipher cipher, VaultViewCipherPageModel.Attachment attachment) - { - if(!Helpers.CanAccessPremium() && !cipher.OrganizationUseTotp) - { - await DisplayAlert(null, AppResources.PremiumRequired, AppResources.Ok); - return; - } - - // 10 MB warning - if(attachment.Size >= 10485760 && !(await DisplayAlert( - null, string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), - AppResources.Yes, AppResources.No))) - { - return; - } - - if(!_deviceActionService.CanOpenFile(attachment.Name)) - { - await DisplayAlert(null, AppResources.UnableToOpenFile, AppResources.Ok); - return; - } - - await _deviceActionService.ShowLoadingAsync(AppResources.Downloading); - var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment.Url, attachment.Key, - cipher.OrganizationId); - await _deviceActionService.HideLoadingAsync(); - - if(data == null) - { - await DisplayAlert(null, AppResources.UnableToDownloadFile, AppResources.Ok); - return; - } - - if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.Name)) - { - await DisplayAlert(null, AppResources.UnableToOpenFile, AppResources.Ok); - return; - } - } - - private void NotesCell_Tapped(object sender, EventArgs e) - { - Copy(Model.Notes, AppResources.Notes); - } - - private void Copy(string copyText, string alertLabel) - { - _deviceActionService.CopyToClipboard(copyText); - _deviceActionService.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); - } - - private void TotpTick(string totpKey, int interval) - { - var now = Helpers.EpocUtcNow() / 1000; - var mod = now % interval; - Model.LoginTotpSecond = (int)(interval - mod); - - if(mod == 0) - { - Model.LoginTotpCode = Crypto.Totp(totpKey); - } - } - - private class EditCipherToolBarItem : ExtendedToolbarItem - { - private readonly VaultViewCipherPage _page; - private readonly string _cipherId; - - public EditCipherToolBarItem(VaultViewCipherPage page, string cipherId) - { - _page = page; - _cipherId = cipherId; - Text = AppResources.Edit; - Icon = Helpers.ToolbarImage("cog.png"); - ClickAction = async () => await ClickedItem(); - } - - private async Task ClickedItem() - { - var page = new VaultEditCipherPage(_cipherId); - await _page.Navigation.PushForDeviceAsync(page); - } - } - - public class AttachmentViewCell : LabeledRightDetailCell, IDisposable - { - private readonly Action _tapped; - - public AttachmentViewCell(VaultViewCipherPageModel.Attachment attachment, Action tappedAction) - { - _tapped = tappedAction; - Label.Text = attachment.Name; - Detail.Text = attachment.SizeName; - Icon.Source = "download.png"; - BackgroundColor = Color.White; - Detail.MinimumWidthRequest = 100; - } - - public void InitEvents() - { - Tapped += AttachmentViewCell_Tapped; - } - - public void Dispose() - { - Tapped -= AttachmentViewCell_Tapped; - } - - private void AttachmentViewCell_Tapped(object sender, EventArgs e) - { - _tapped?.Invoke(); - } - } - - public class FieldViewCell : LabeledValueCell - { - public FieldViewCell(VaultViewCipherPage page, VaultViewCipherPageModel.Field field) - : base(field.Name, field.Value == "true" ? "✓" : "-") - { - Init(page, field, null); - } - - public FieldViewCell(VaultViewCipherPage page, VaultViewCipherPageModel.Field field, bool? a) - : base(field.Name, field.Value, "clipboard.png") - { - Init(page, field, Button1); - } - - public FieldViewCell(VaultViewCipherPage page, VaultViewCipherPageModel.Field field, bool? a, bool? b) - : base(field.Name, field.MaskedValue, string.Empty, "clipboard.png") - { - Value.FontFamily = Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - Button1.Image = "eye"; - Button1.Command = new Command(() => - { - field.Revealed = !field.Revealed; - if(field.Revealed) - { - Button1.Image = "eye_slash.png"; - Value.Text = field.Value; - } - else - { - Button1.Image = "eye.png"; - Value.Text = field.MaskedValue; - } - }); - - Init(page, field, Button2); - } - - private void Init(VaultViewCipherPage page, VaultViewCipherPageModel.Field field, ExtendedButton copyButton) - { - Value.LineBreakMode = LineBreakMode.WordWrap; - if(copyButton != null) - { - copyButton.Command = new Command(() => page.Copy(field.Value, field.Name)); - } - } - } - - public class UriViewCell : LabeledValueCell - { - public UriViewCell(VaultViewCipherPage page, VaultViewCipherPageModel.LoginUri uri) - : base(uri.Label, uri.Host, uri.ShowLaunch ? "launch.png" : null, "clipboard.png") - { - Value.LineBreakMode = LineBreakMode.TailTruncation; - if(Button1 != null) - { - Button1.Command = new Command(async () => - { - if(Device.RuntimePlatform == Device.Android && uri.IsApp) - { - await page._deviceActionService.LaunchAppAsync(uri.Value, page); - } - else if(uri.IsWebsite) - { - Device.OpenUri(new Uri(uri.Value)); - } - }); - } - Button2.Command = new Command(() => page.Copy(uri.Value, AppResources.URI)); - } - } - } -} diff --git a/src/App/Pages/Vault/ViewPage.xaml b/src/App/Pages/Vault/ViewPage.xaml new file mode 100644 index 000000000..5a79dae7b --- /dev/null +++ b/src/App/Pages/Vault/ViewPage.xaml @@ -0,0 +1,633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/ViewPage.xaml.cs b/src/App/Pages/Vault/ViewPage.xaml.cs new file mode 100644 index 000000000..d3dbb9cb9 --- /dev/null +++ b/src/App/Pages/Vault/ViewPage.xaml.cs @@ -0,0 +1,143 @@ +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System.Collections.Generic; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class ViewPage : BaseContentPage + { + private readonly IBroadcasterService _broadcasterService; + private ViewPageViewModel _vm; + + public ViewPage(string cipherId) + { + InitializeComponent(); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _vm = BindingContext as ViewPageViewModel; + _vm.Page = this; + _vm.CipherId = cipherId; + SetActivityIndicator(_mainContent); + + if(Device.RuntimePlatform == Device.iOS) + { + _absLayout.Children.Remove(_fab); + } + else + { + ToolbarItems.RemoveAt(0); + _fab.Clicked = EditButton_Clicked; + _mainLayout.Padding = new Thickness(0, 0, 0, 75); + } + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + _broadcasterService.Subscribe(nameof(ViewPage), async (message) => + { + if(message.Command == "syncCompleted") + { + var data = message.Data as Dictionary; + if(data.ContainsKey("successfully")) + { + var success = data["successfully"] as bool?; + if(success.HasValue && success.Value) + { + await _vm.LoadAsync(); + } + } + } + }); + await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync(), _mainContent); + if(Device.RuntimePlatform == Device.Android) + { + if(_vm.Cipher.OrganizationId == null) + { + if(ToolbarItems.Contains(_collectionsItem)) + { + ToolbarItems.Remove(_collectionsItem); + } + if(!ToolbarItems.Contains(_shareItem)) + { + ToolbarItems.Insert(1, _shareItem); + } + } + else + { + if(ToolbarItems.Contains(_shareItem)) + { + ToolbarItems.Remove(_shareItem); + } + if(!ToolbarItems.Contains(_collectionsItem)) + { + ToolbarItems.Insert(1, _collectionsItem); + } + } + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _broadcasterService.Unsubscribe(nameof(ViewPage)); + _vm.CleanUp(); + } + + private async void PasswordHistory_Tapped(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await Navigation.PushModalAsync(new NavigationPage(new PasswordHistoryPage(_vm.CipherId))); + } + } + + private async void EditToolbarItem_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId))); + } + } + + private void EditButton_Clicked(object sender, System.EventArgs e) + { + EditToolbarItem_Clicked(sender, e); + } + + private async void Attachments_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new AttachmentsPage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + + private async void Share_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new SharePage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + + private async void Delete_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + await _vm.DeleteAsync(); + } + } + + private async void Collections_Clicked(object sender, System.EventArgs e) + { + if(DoOnce()) + { + var page = new CollectionsPage(_vm.CipherId); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + } + } +} diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs new file mode 100644 index 000000000..9ab72ccd7 --- /dev/null +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -0,0 +1,506 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class ViewPageViewModel : BaseViewModel + { + private readonly IDeviceActionService _deviceActionService; + private readonly ICipherService _cipherService; + private readonly IUserService _userService; + private readonly ITotpService _totpService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IAuditService _auditService; + private readonly IMessagingService _messagingService; + private CipherView _cipher; + private List _fields; + private bool _canAccessPremium; + private bool _showPassword; + private bool _showCardCode; + private string _totpCode; + private string _totpCodeFormatted; + private string _totpSec; + private bool _totpLow; + private DateTime? _totpInterval = null; + + public ViewPageViewModel() + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _userService = ServiceContainer.Resolve("userService"); + _totpService = ServiceContainer.Resolve("totpService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _auditService = ServiceContainer.Resolve("auditService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + CopyCommand = new Command((id) => CopyAsync(id, null)); + CopyUriCommand = new Command(CopyUri); + CopyFieldCommand = new Command(CopyField); + LaunchUriCommand = new Command(LaunchUri); + TogglePasswordCommand = new Command(TogglePassword); + ToggleCardCodeCommand = new Command(ToggleCardCode); + CheckPasswordCommand = new Command(CheckPasswordAsync); + DownloadAttachmentCommand = new Command(DownloadAttachmentAsync); + + PageTitle = AppResources.ViewItem; + } + + public Command CopyCommand { get; set; } + public Command CopyUriCommand { get; set; } + public Command CopyFieldCommand { get; set; } + public Command LaunchUriCommand { get; set; } + public Command TogglePasswordCommand { get; set; } + public Command ToggleCardCodeCommand { get; set; } + public Command CheckPasswordCommand { get; set; } + public Command DownloadAttachmentCommand { get; set; } + public string CipherId { get; set; } + public CipherView Cipher + { + get => _cipher; + set => SetProperty(ref _cipher, value, + additionalPropertyNames: new string[] + { + nameof(IsLogin), + nameof(IsIdentity), + nameof(IsCard), + nameof(IsSecureNote), + nameof(ShowUris), + nameof(ShowAttachments), + nameof(ShowTotp), + nameof(ColoredPassword), + nameof(UpdatedText), + nameof(PasswordUpdatedText), + nameof(PasswordHistoryText), + nameof(ShowIdentityAddress), + }); + } + public List Fields + { + get => _fields; + set => SetProperty(ref _fields, value); + } + public bool CanAccessPremium + { + get => _canAccessPremium; + set => SetProperty(ref _canAccessPremium, value); + } + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + public bool ShowCardCode + { + get => _showCardCode; + set => SetProperty(ref _showCardCode, value, + additionalPropertyNames: new string[] + { + nameof(ShowCardCodeIcon) + }); + } + public bool IsLogin => Cipher?.Type == Core.Enums.CipherType.Login; + public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity; + public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card; + public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote; + public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password); + public FormattedString UpdatedText + { + get + { + var fs = new FormattedString(); + fs.Spans.Add(new Span + { + Text = string.Format("{0}:", AppResources.DateUpdated), + FontAttributes = FontAttributes.Bold + }); + fs.Spans.Add(new Span + { + Text = string.Format(" {0} {1}", + Cipher.RevisionDate.ToShortDateString(), + Cipher.RevisionDate.ToShortTimeString()) + }); + return fs; + } + } + public FormattedString PasswordUpdatedText + { + get + { + var fs = new FormattedString(); + fs.Spans.Add(new Span + { + Text = string.Format("{0}:", AppResources.DatePasswordUpdated), + FontAttributes = FontAttributes.Bold + }); + fs.Spans.Add(new Span + { + Text = string.Format(" {0} {1}", + Cipher.PasswordRevisionDisplayDate?.ToShortDateString(), + Cipher.PasswordRevisionDisplayDate?.ToShortTimeString()) + }); + return fs; + } + } + public FormattedString PasswordHistoryText + { + get + { + var fs = new FormattedString(); + fs.Spans.Add(new Span + { + Text = string.Format("{0}:", AppResources.PasswordHistory), + FontAttributes = FontAttributes.Bold + }); + fs.Spans.Add(new Span + { + Text = string.Format(" {0}", Cipher.PasswordHistory.Count.ToString()), + TextColor = (Color)Application.Current.Resources["PrimaryColor"] + }); + return fs; + } + } + public bool ShowUris => IsLogin && Cipher.Login.HasUris; + public bool ShowIdentityAddress => IsIdentity && ( + !string.IsNullOrWhiteSpace(Cipher.Identity.Address1) || + !string.IsNullOrWhiteSpace(Cipher.Identity.City) || + !string.IsNullOrWhiteSpace(Cipher.Identity.Country)); + public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null); + public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) && + !string.IsNullOrWhiteSpace(TotpCodeFormatted); + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string ShowCardCodeIcon => ShowCardCode ? "" : ""; + public string TotpCodeFormatted + { + get => _totpCodeFormatted; + set => SetProperty(ref _totpCodeFormatted, value, + additionalPropertyNames: new string[] + { + nameof(ShowTotp) + }); + } + public string TotpSec + { + get => _totpSec; + set => SetProperty(ref _totpSec, value); + } + public bool TotpLow + { + get => _totpLow; + set + { + SetProperty(ref _totpLow, value); + Page.Resources["textTotp"] = Application.Current.Resources[value ? "text-danger" : "text-default"]; + } + } + + public async Task LoadAsync() + { + CleanUp(); + var cipher = await _cipherService.GetAsync(CipherId); + Cipher = await cipher.DecryptAsync(); + CanAccessPremium = await _userService.CanAccessPremiumAsync(); + Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(f)).ToList(); + + if(Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) && + (Cipher.OrganizationUseTotp || CanAccessPremium)) + { + await TotpUpdateCodeAsync(); + var interval = _totpService.GetTimeInterval(Cipher.Login.Totp); + await TotpTickAsync(interval); + _totpInterval = DateTime.UtcNow; + Device.StartTimer(new TimeSpan(0, 0, 1), () => + { + if(_totpInterval == null) + { + return false; + } + var task = TotpTickAsync(interval); + return true; + }); + } + } + + public void CleanUp() + { + _totpInterval = null; + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + } + + public void ToggleCardCode() + { + ShowCardCode = !ShowCardCode; + } + + public async Task DeleteAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete, + null, AppResources.Yes, AppResources.No); + if(!confirmed) + { + return false; + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); + await _cipherService.DeleteWithServerAsync(Cipher.Id); + await _deviceActionService.HideLoadingAsync(); + _platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted); + _messagingService.Send("deletedCipher"); + return true; + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + return false; + } + + private async Task TotpUpdateCodeAsync() + { + if(Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null) + { + _totpInterval = null; + return; + } + _totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp); + if(_totpCode != null) + { + if(_totpCode.Length > 4) + { + var half = (int)Math.Floor(_totpCode.Length / 2M); + TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half), + _totpCode.Substring(half)); + } + else + { + TotpCodeFormatted = _totpCode; + } + } + else + { + TotpCodeFormatted = null; + _totpInterval = null; + } + } + + private async Task TotpTickAsync(int intervalSeconds) + { + var epoc = CoreHelpers.EpocUtcNow() / 1000; + var mod = epoc % intervalSeconds; + var totpSec = intervalSeconds - mod; + TotpSec = totpSec.ToString(); + TotpLow = totpSec < 7; + if(mod == 0) + { + await TotpUpdateCodeAsync(); + } + } + + private async void CheckPasswordAsync() + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + if(string.IsNullOrWhiteSpace(Cipher.Login?.Password)) + { + return; + } + await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword); + var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password); + await _deviceActionService.HideLoadingAsync(); + if(matches > 0) + { + await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed, + matches.ToString("N0"))); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe); + } + } + + private async void DownloadAttachmentAsync(AttachmentView attachment) + { + if(!(Page as BaseContentPage).DoOnce()) + { + return; + } + if(Cipher.OrganizationId == null && !CanAccessPremium) + { + await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired); + return; + } + if(attachment.FileSize >= 10485760) // 10 MB + { + var confirmed = await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null, + AppResources.Yes, AppResources.No); + if(!confirmed) + { + return; + } + } + if(!_deviceActionService.CanOpenFile(attachment.FileName)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); + return; + } + + await _deviceActionService.ShowLoadingAsync(AppResources.Downloading); + try + { + var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId); + await _deviceActionService.HideLoadingAsync(); + if(data == null) + { + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile); + return; + } + if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); + return; + } + } + catch + { + await _deviceActionService.HideLoadingAsync(); + } + } + + private async void CopyAsync(string id, string text = null) + { + string name = null; + if(id == "LoginUsername") + { + text = Cipher.Login.Username; + name = AppResources.Username; + } + else if(id == "LoginPassword") + { + text = Cipher.Login.Password; + name = AppResources.Password; + } + else if(id == "LoginTotp") + { + text = _totpCode; + name = AppResources.VerificationCodeTotp; + } + else if(id == "LoginUri") + { + name = AppResources.URI; + } + else if(id == "FieldValue") + { + name = AppResources.Value; + } + else if(id == "CardNumber") + { + text = Cipher.Card.Number; + name = AppResources.Number; + } + else if(id == "CardCode") + { + text = Cipher.Card.Code; + name = AppResources.SecurityCode; + } + + if(text != null) + { + await _platformUtilsService.CopyToClipboardAsync(text); + if(!string.IsNullOrWhiteSpace(name)) + { + _platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name)); + } + } + } + + private void CopyUri(LoginUriView uri) + { + CopyAsync("LoginUri", uri.Uri); + } + + private void CopyField(FieldView field) + { + CopyAsync("FieldValue", field.Value); + } + + private void LaunchUri(LoginUriView uri) + { + if(uri.CanLaunch && (Page as BaseContentPage).DoOnce()) + { + _platformUtilsService.LaunchUri(uri.LaunchUri); + } + } + } + + public class ViewPageFieldViewModel : ExtendedViewModel + { + private FieldView _field; + private bool _showHiddenValue; + + public ViewPageFieldViewModel(FieldView field) + { + Field = field; + ToggleHiddenValueCommand = new Command(ToggleHiddenValue); + } + + public FieldView Field + { + get => _field; + set => SetProperty(ref _field, value, + additionalPropertyNames: new string[] + { + nameof(ValueText), + nameof(IsBooleanType), + nameof(IsHiddenType), + nameof(IsTextType), + nameof(ShowCopyButton), + }); + } + + public bool ShowHiddenValue + { + get => _showHiddenValue; + set => SetProperty(ref _showHiddenValue, value, + additionalPropertyNames: new string[] + { + nameof(ShowHiddenValueIcon) + }); + } + + public Command ToggleHiddenValueCommand { get; set; } + + public string ValueText => IsBooleanType ? (_field.Value == "true" ? "" : "") : _field.Value; + public string ShowHiddenValueIcon => _showHiddenValue ? "" : ""; + public bool IsTextType => _field.Type == Core.Enums.FieldType.Text; + public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean; + public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden; + public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean && + !string.IsNullOrWhiteSpace(_field.Value); + + public void ToggleHiddenValue() + { + ShowHiddenValue = !ShowHiddenValue; + } + } +} diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs deleted file mode 100644 index e5ab2743d..000000000 --- a/src/App/Repositories/AccountsApiRepository.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Connectivity.Abstractions; -using Newtonsoft.Json; -using Bit.App.Utilities; - -namespace Bit.App.Repositories -{ - public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository - { - public AccountsApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/accounts"; - - public virtual async Task> PostPreloginAsync(PreloginRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/prelogin")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task PostRegisterAsync(RegisterRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/register")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task PostPasswordHintAsync(PasswordHintRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/password-hint")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task> GetAccountRevisionDateAsync() - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/revision-date")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if(responseContent.Contains("null")) - { - return ApiResult.Success(null, response.StatusCode); - } - - long ms; - if(!long.TryParse(responseContent, out ms)) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - return ApiResult.Success(Helpers.Epoc.AddMilliseconds(ms), response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task> GetProfileAsync() - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/profile")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/ApiRepository.cs b/src/App/Repositories/ApiRepository.cs deleted file mode 100644 index 271c29c20..000000000 --- a/src/App/Repositories/ApiRepository.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Newtonsoft.Json; -using Plugin.Connectivity.Abstractions; - -namespace Bit.App.Repositories -{ - public abstract class ApiRepository : BaseApiRepository, IApiRepository - where TId : IEquatable - where TRequest : class - where TResponse : class - { - public ApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - public virtual async Task> GetByIdAsync(TId id) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/", id)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task>> GetAsync() - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected>(); - } - - var tokenStateResponse = await HandleTokenStateAsync>(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync>(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject>(responseContent); - return ApiResult>.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException>(); - } - } - } - - public virtual async Task> PostAsync(TRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task> PutAsync(TId id, TRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Put, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/", id)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task DeleteAsync(TId id) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Delete, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/", id)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/AttachmentRepository.cs b/src/App/Repositories/AttachmentRepository.cs deleted file mode 100644 index 36b4f732a..000000000 --- a/src/App/Repositories/AttachmentRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; - -namespace Bit.App.Repositories -{ - public class AttachmentRepository : Repository, IAttachmentRepository - { - public AttachmentRepository(ISqlService sqlService) - : base(sqlService) - { } - - public Task> GetAllByCipherIdAsync(string cipherId) - { - var attachments = Connection.Table().Where(a => a.LoginId == cipherId) - .Cast(); - return Task.FromResult(attachments); - } - - public Task> GetAllByUserIdAsync(string userId) - { - var attachments = Connection.Query(@" - SELECT - A.* - FROM - Attachment AS A - INNER JOIN - Site AS S ON S.Id = A.LoginId - WHERE - S.UserId = ?", userId); - return Task.FromResult>(attachments); - } - } -} diff --git a/src/App/Repositories/BaseApiRepository.cs b/src/App/Repositories/BaseApiRepository.cs deleted file mode 100644 index 4a26fc665..000000000 --- a/src/App/Repositories/BaseApiRepository.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Models.Api; -using Newtonsoft.Json; -using Plugin.Connectivity.Abstractions; -using Bit.App.Abstractions; -using System.Net; -using Newtonsoft.Json.Linq; - -namespace Bit.App.Repositories -{ - public abstract class BaseApiRepository - { - public BaseApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - { - Connectivity = connectivity; - HttpService = httpService; - TokenService = tokenService; - } - - protected IConnectivity Connectivity { get; private set; } - protected IHttpService HttpService { get; private set; } - protected ITokenService TokenService { get; private set; } - protected abstract string ApiRoute { get; } - - protected async Task HandleTokenStateAsync() - { - return await HandleTokenStateAsync( - () => ApiResult.Success(HttpStatusCode.OK), - () => HandledWebException(), - (r) => HandleErrorAsync(r)); - } - - protected async Task> HandleTokenStateAsync() - { - return await HandleTokenStateAsync( - () => ApiResult.Success(default(T), HttpStatusCode.OK), - () => HandledWebException(), - (r) => HandleErrorAsync(r)); - } - - private async Task HandleTokenStateAsync(Func success, Func webException, - Func> error) - { - if(TokenService.TokenNeedsRefresh && !string.IsNullOrWhiteSpace(TokenService.RefreshToken)) - { - using(var client = HttpService.IdentityClient) - { - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, "/connect/token")), - Content = new FormUrlEncodedContent(new Dictionary - { - { "grant_type", "refresh_token" }, - { "client_id", "mobile" }, - { "refresh_token", TokenService.RefreshToken } - }) - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - if(response.StatusCode == HttpStatusCode.BadRequest) - { - response.StatusCode = HttpStatusCode.Unauthorized; - } - - return await error.Invoke(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var tokenResponse = JsonConvert.DeserializeObject(responseContent); - TokenService.Token = tokenResponse.AccessToken; - TokenService.RefreshToken = tokenResponse.RefreshToken; - } - catch - { - return webException.Invoke(); - } - } - } - - return success.Invoke(); - } - - protected ApiResult HandledNotConnected() - { - return ApiResult.Failed(HttpStatusCode.RequestTimeout, - new ApiError { Message = "Not connected to the internet." }); - } - - protected ApiResult HandledNotConnected() - { - return ApiResult.Failed(HttpStatusCode.RequestTimeout, - new ApiError { Message = "Not connected to the internet." }); - } - - protected ApiResult HandledWebException() - { - return ApiResult.Failed(HttpStatusCode.BadGateway, - new ApiError { Message = "There is a problem connecting to the server." }); - } - - protected ApiResult HandledWebException() - { - return ApiResult.Failed(HttpStatusCode.BadGateway, - new ApiError { Message = "There is a problem connecting to the server." }); - } - - protected async Task> HandleErrorAsync(HttpResponseMessage response) - { - try - { - var errors = await ParseErrorsAsync(response).ConfigureAwait(false); - return ApiResult.Failed(response.StatusCode, errors.ToArray()); - } - catch - { } - - return ApiResult.Failed(response.StatusCode, - new ApiError { Message = "An unknown error has occurred." }); - } - - protected async Task HandleErrorAsync(HttpResponseMessage response) - { - try - { - var errors = await ParseErrorsAsync(response).ConfigureAwait(false); - return ApiResult.Failed(response.StatusCode, errors.ToArray()); - } - catch - { } - - return ApiResult.Failed(response.StatusCode, - new ApiError { Message = "An unknown error has occurred." }); - } - - private async Task> ParseErrorsAsync(HttpResponseMessage response) - { - var errors = new List(); - var statusCode = (int)response.StatusCode; - if(statusCode >= 400 && statusCode <= 500) - { - ErrorResponse errorResponseModel = null; - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if(!string.IsNullOrWhiteSpace(responseContent)) - { - var errorResponse = JObject.Parse(responseContent); - if(errorResponse["ErrorModel"] != null && errorResponse["ErrorModel"]["Message"] != null) - { - errorResponseModel = errorResponse["ErrorModel"].ToObject(); - } - else if(errorResponse["Message"] != null) - { - errorResponseModel = errorResponse.ToObject(); - } - } - - if(errorResponseModel != null) - { - if((errorResponseModel.ValidationErrors?.Count ?? 0) > 0) - { - foreach(var valError in errorResponseModel.ValidationErrors) - { - foreach(var errorMessage in valError.Value) - { - errors.Add(new ApiError { Message = errorMessage }); - } - } - } - else - { - errors.Add(new ApiError { Message = errorResponseModel.Message }); - } - } - } - - if(errors.Count == 0) - { - errors.Add(new ApiError { Message = "An unknown error has occurred." }); - } - - return errors; - } - } -} diff --git a/src/App/Repositories/BaseRepository.cs b/src/App/Repositories/BaseRepository.cs deleted file mode 100644 index 623bae693..000000000 --- a/src/App/Repositories/BaseRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.App.Abstractions; -using SQLite; - -namespace Bit.App.Repositories -{ - public abstract class BaseRepository - { - public BaseRepository(ISqlService sqlService) - { - Connection = sqlService.GetConnection(); - } - - protected SQLiteConnection Connection { get; private set; } - } -} diff --git a/src/App/Repositories/CipherApiRepository.cs b/src/App/Repositories/CipherApiRepository.cs deleted file mode 100644 index c527edcd9..000000000 --- a/src/App/Repositories/CipherApiRepository.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Newtonsoft.Json; -using Plugin.Connectivity.Abstractions; -using System.IO; - -namespace Bit.App.Repositories -{ - public class CipherApiRepository : ApiRepository, ICipherApiRepository - { - public CipherApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/ciphers"; - - public virtual async Task> PostAttachmentAsync(string cipherId, byte[] data, - string key, string fileName) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - using(var content = new MultipartFormDataContent("--BWMobileFormBoundary" + DateTime.UtcNow.Ticks)) - { - content.Add(new StringContent(key), "key"); - content.Add(new StreamContent(new MemoryStream(data)), "data", fileName); - - var requestMessage = new TokenHttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/", cipherId, "/attachment")), - Content = content - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task DeleteAttachmentAsync(string cipherId, string attachmentId) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Delete, - RequestUri = new Uri( - string.Concat(client.BaseAddress, ApiRoute, "/", cipherId, "/attachment/", attachmentId)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/CipherCollectionRepository.cs b/src/App/Repositories/CipherCollectionRepository.cs deleted file mode 100644 index c2bcd8d89..000000000 --- a/src/App/Repositories/CipherCollectionRepository.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; -using System.Collections.Generic; -using System.Linq; - -namespace Bit.App.Repositories -{ - public class CipherCollectionRepository : BaseRepository, ICipherCollectionRepository - { - public CipherCollectionRepository(ISqlService sqlService) - : base(sqlService) - { } - - public Task> GetAllByUserIdAsync(string userId) - { - var cipherCollections = Connection.Table().Where(f => f.UserId == userId) - .Cast(); - return Task.FromResult(cipherCollections); - } - - public Task> GetAllByUserIdCollectionAsync(string userId, string collectionId) - { - var cipherCollections = Connection.Table().Where( - f => f.UserId == userId && f.CollectionId == collectionId).Cast(); - return Task.FromResult(cipherCollections); - } - - public virtual Task InsertAsync(CipherCollectionData obj) - { - Connection.Insert(obj); - return Task.FromResult(0); - } - - public virtual Task DeleteAsync(CipherCollectionData obj) - { - Connection.Delete(obj.Id); - return Task.FromResult(0); - } - - public virtual Task DeleteByUserIdAsync(string userId) - { - Connection.Execute("DELETE FROM CipherCollection WHERE UserId = ?", userId); - return Task.FromResult(0); - } - } -} diff --git a/src/App/Repositories/CipherRepository.cs b/src/App/Repositories/CipherRepository.cs deleted file mode 100644 index d210b4285..000000000 --- a/src/App/Repositories/CipherRepository.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; - -namespace Bit.App.Repositories -{ - public class CipherRepository : Repository, ICipherRepository - { - public CipherRepository(ISqlService sqlService) - : base(sqlService) - { } - - public Task> GetAllByUserIdAsync(string userId) - { - var ciphers = Connection.Table().Where(l => l.UserId == userId).Cast(); - return Task.FromResult(ciphers); - } - - public Task> GetAllByUserIdAsync(string userId, bool favorite) - { - var ciphers = Connection.Table().Where(l => l.UserId == userId && l.Favorite == favorite) - .Cast(); - return Task.FromResult(ciphers); - } - } -} diff --git a/src/App/Repositories/CollectionRepository.cs b/src/App/Repositories/CollectionRepository.cs deleted file mode 100644 index 9924e5bb4..000000000 --- a/src/App/Repositories/CollectionRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; - -namespace Bit.App.Repositories -{ - public class CollectionRepository : Repository, ICollectionRepository - { - public CollectionRepository(ISqlService sqlService) - : base(sqlService) - { } - - public Task> GetAllByUserIdAsync(string userId) - { - var folders = Connection.Table().Where(f => f.UserId == userId).Cast(); - return Task.FromResult(folders); - } - } -} diff --git a/src/App/Repositories/ConnectApiRepository.cs b/src/App/Repositories/ConnectApiRepository.cs deleted file mode 100644 index 503473cfc..000000000 --- a/src/App/Repositories/ConnectApiRepository.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Newtonsoft.Json; -using Plugin.Connectivity.Abstractions; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using Bit.App.Enums; -using Bit.App.Utilities; - -namespace Bit.App.Repositories -{ - public class ConnectApiRepository : BaseApiRepository, IConnectApiRepository - { - public ConnectApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/connect"; - - public virtual async Task> PostTokenAsync(TokenRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.IdentityClient) - { - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/token")), - Content = new FormUrlEncodedContent(requestObj.ToIdentityTokenRequest()) - }; - - requestMessage.Headers.Add("Device-Type", ((int)Helpers.OnPlatform(iOS: DeviceType.iOS, - Android: DeviceType.Android, Windows: DeviceType.UWP)).ToString()); - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if(!response.IsSuccessStatusCode) - { - var errorResponse = JObject.Parse(responseContent); - if(errorResponse["TwoFactorProviders2"] != null) - { - TokenService.SetTwoFactorToken(requestObj.Email, null); - - return ApiResult.Success(new TokenResponse - { - TwoFactorProviders2 = - errorResponse["TwoFactorProviders2"] - .ToObject>>() - }, response.StatusCode); - } - - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/DeviceApiRepository.cs b/src/App/Repositories/DeviceApiRepository.cs deleted file mode 100644 index 06c86024c..000000000 --- a/src/App/Repositories/DeviceApiRepository.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Newtonsoft.Json; -using Plugin.Connectivity.Abstractions; -using System.Net; - -namespace Bit.App.Repositories -{ - public class DeviceApiRepository : ApiRepository, IDeviceApiRepository - { - public DeviceApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/devices"; - - public virtual async Task PutTokenAsync(string identifier, DeviceTokenRequest request) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(request) - { - Method = HttpMethod.Put, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/identifier/", identifier, "/token")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - - public virtual async Task PutClearTokenAsync(string identifier) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage - { - Method = HttpMethod.Put, - RequestUri = new Uri( - string.Concat(client.BaseAddress, ApiRoute, "/identifier/", identifier, "/clear-token")) - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/FolderApiRepository.cs b/src/App/Repositories/FolderApiRepository.cs deleted file mode 100644 index 5519af938..000000000 --- a/src/App/Repositories/FolderApiRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Connectivity.Abstractions; - -namespace Bit.App.Repositories -{ - public class FolderApiRepository : ApiRepository, IFolderApiRepository - { - public FolderApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/folders"; - } -} diff --git a/src/App/Repositories/FolderRepository.cs b/src/App/Repositories/FolderRepository.cs deleted file mode 100644 index 6af7d9cf3..000000000 --- a/src/App/Repositories/FolderRepository.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; - -namespace Bit.App.Repositories -{ - public class FolderRepository : Repository, IFolderRepository - { - public FolderRepository(ISqlService sqlService) - : base(sqlService) - { } - - public Task> GetAllByUserIdAsync(string userId) - { - var folders = Connection.Table().Where(f => f.UserId == userId).Cast(); - return Task.FromResult(folders); - } - - public override Task DeleteAsync(string id) - { - var now = DateTime.UtcNow; - DeleteWithCipherUpdateAsync(id, now); - return Task.FromResult(0); - } - - public Task DeleteWithCipherUpdateAsync(string id, DateTime revisionDate) - { - Connection.RunInTransaction(() => - { - Connection.Execute("UPDATE Site SET FolderId = ?, RevisionDateTime = ? WHERE FolderId = ?", null, revisionDate, id); - Connection.Delete(id); - }); - - return Task.FromResult(0); - } - } -} diff --git a/src/App/Repositories/Repository.cs b/src/App/Repositories/Repository.cs deleted file mode 100644 index 92f3696a5..000000000 --- a/src/App/Repositories/Repository.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; - -namespace Bit.App.Repositories -{ - public abstract class Repository : BaseRepository, IRepository - where TId : IEquatable - where T : class, IDataObject, new() - { - public Repository(ISqlService sqlService) - : base(sqlService) - { } - - public virtual Task GetByIdAsync(TId id) - { - return Task.FromResult(Connection.Find(id)); - } - - public virtual Task> GetAllAsync() - { - return Task.FromResult(Connection.Table().Cast()); - } - - public virtual Task InsertAsync(T obj) - { - Connection.Insert(obj); - return Task.FromResult(0); - } - - public virtual Task UpdateAsync(T obj) - { - Connection.Update(obj); - return Task.FromResult(0); - } - - public virtual Task UpsertAsync(T obj) - { - Connection.InsertOrReplace(obj); - return Task.FromResult(0); - } - - public virtual async Task DeleteAsync(T obj) - { - await DeleteAsync(obj.Id); - } - - public virtual Task DeleteAsync(TId id) - { - Connection.Delete(id); - return Task.FromResult(0); - } - } -} diff --git a/src/App/Repositories/SettingsApiRepository.cs b/src/App/Repositories/SettingsApiRepository.cs deleted file mode 100644 index 502871498..000000000 --- a/src/App/Repositories/SettingsApiRepository.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Connectivity.Abstractions; -using Newtonsoft.Json; - -namespace Bit.App.Repositories -{ - public class SettingsApiRepository : BaseApiRepository, ISettingsApiRepository - { - public SettingsApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/settings"; - - public virtual async Task> GetDomains(bool excluded = false) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri( - string.Concat(client.BaseAddress, ApiRoute, "/domains?excluded=", excluded.ToString().ToLowerInvariant())), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/SettingsRepository.cs b/src/App/Repositories/SettingsRepository.cs deleted file mode 100644 index 22ac074e0..000000000 --- a/src/App/Repositories/SettingsRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Bit.App.Abstractions; -using Bit.App.Models.Data; - -namespace Bit.App.Repositories -{ - public class SettingsRepository : Repository, ISettingsRepository - { - public SettingsRepository(ISqlService sqlService) - : base(sqlService) - { } - } -} diff --git a/src/App/Repositories/SyncApiRepository.cs b/src/App/Repositories/SyncApiRepository.cs deleted file mode 100644 index 552c16d69..000000000 --- a/src/App/Repositories/SyncApiRepository.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Connectivity.Abstractions; -using Newtonsoft.Json; - -namespace Bit.App.Repositories -{ - public class SyncApiRepository : BaseApiRepository, ISyncApiRepository - { - public SyncApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/sync"; - - public virtual async Task> Get() - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - var tokenStateResponse = await HandleTokenStateAsync(); - if(!tokenStateResponse.Succeeded) - { - return tokenStateResponse; - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri( - string.Concat(client.BaseAddress, ApiRoute)), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Repositories/TwoFactorApiRepository.cs b/src/App/Repositories/TwoFactorApiRepository.cs deleted file mode 100644 index 50c9695dc..000000000 --- a/src/App/Repositories/TwoFactorApiRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Connectivity.Abstractions; - -namespace Bit.App.Repositories -{ - public class TwoFactorApiRepository : BaseApiRepository, ITwoFactorApiRepository - { - public TwoFactorApiRepository( - IConnectivity connectivity, - IHttpService httpService, - ITokenService tokenService) - : base(connectivity, httpService, tokenService) - { } - - protected override string ApiRoute => "/two-factor"; - - public virtual async Task PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj) - { - if(!Connectivity.IsConnected) - { - return HandledNotConnected(); - } - - using(var client = HttpService.ApiClient) - { - var requestMessage = new TokenHttpRequestMessage(requestObj) - { - Method = HttpMethod.Post, - RequestUri = new Uri(string.Concat(client.BaseAddress, ApiRoute, "/send-email-login")), - }; - - try - { - var response = await client.SendAsync(requestMessage).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync(response).ConfigureAwait(false); - } - - return ApiResult.Success(response.StatusCode); - } - catch - { - return HandledWebException(); - } - } - } - } -} diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 1b23eb613..253d7d0e0 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -168,6 +168,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to All Items. + /// + public static string AllItems { + get { + return ResourceManager.GetString("AllItems", resourceCulture); + } + } + /// /// Looks up a localized string similar to An error has occurred.. /// @@ -780,6 +789,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Cards. + /// + public static string Cards { + get { + return ResourceManager.GetString("Cards", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change Email. /// @@ -816,6 +834,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Checking password.... + /// + public static string CheckingPassword { + get { + return ResourceManager.GetString("CheckingPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Check if password has been exposed.. + /// + public static string CheckPassword { + get { + return ResourceManager.GetString("CheckPassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose File. /// @@ -834,6 +870,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Clear. + /// + public static string Clear { + get { + return ResourceManager.GetString("Clear", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close. /// @@ -1338,6 +1383,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Export Vault. + /// + public static string ExportVault { + get { + return ResourceManager.GetString("ExportVault", resourceCulture); + } + } + /// /// Looks up a localized string similar to Extension Activated!. /// @@ -1572,6 +1626,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Fingerprint Phrase. + /// + public static string FingerprintPhrase { + get { + return ResourceManager.GetString("FingerprintPhrase", resourceCulture); + } + } + /// /// Looks up a localized string similar to First Name. /// @@ -1653,6 +1716,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Generator. + /// + public static string Generator { + get { + return ResourceManager.GetString("Generator", resourceCulture); + } + } + /// /// Looks up a localized string similar to Get your master password hint. /// @@ -1734,6 +1806,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Identities. + /// + public static string Identities { + get { + return ResourceManager.GetString("Identities", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identity Name. + /// + public static string IdentityName { + get { + return ResourceManager.GetString("IdentityName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Identity Server URL. /// @@ -1788,6 +1878,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Invalid email address.. + /// + public static string InvalidEmail { + get { + return ResourceManager.GetString("InvalidEmail", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid Master Password. Try again.. /// @@ -1842,6 +1941,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Item has been shared.. + /// + public static string ItemShared { + get { + return ResourceManager.GetString("ItemShared", resourceCulture); + } + } + /// /// Looks up a localized string similar to Item updated.. /// @@ -1941,6 +2049,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Lock Now. + /// + public static string LockNow { + get { + return ResourceManager.GetString("LockNow", resourceCulture); + } + } + /// /// Looks up a localized string similar to 15 minutes. /// @@ -1968,6 +2085,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to 30 minutes. + /// + public static string LockOption30Minutes { + get { + return ResourceManager.GetString("LockOption30Minutes", resourceCulture); + } + } + /// /// Looks up a localized string similar to 4 hours. /// @@ -1977,6 +2103,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to 5 minutes. + /// + public static string LockOption5Minutes { + get { + return ResourceManager.GetString("LockOption5Minutes", resourceCulture); + } + } + /// /// Looks up a localized string similar to Immediately. /// @@ -1995,6 +2130,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Logged in as {0}.. + /// + public static string LoggedInAs { + get { + return ResourceManager.GetString("LoggedInAs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Logging in.... /// @@ -2031,6 +2175,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Logins. + /// + public static string Logins { + get { + return ResourceManager.GetString("Logins", resourceCulture); + } + } + /// /// Looks up a localized string similar to Login Unavailable. /// @@ -2202,6 +2355,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Miscellaneous. + /// + public static string Miscellaneous { + get { + return ResourceManager.GetString("Miscellaneous", resourceCulture); + } + } + /// /// Looks up a localized string similar to More. /// @@ -2220,6 +2382,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Move Down. + /// + public static string MoveDown { + get { + return ResourceManager.GetString("MoveDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move Up. + /// + public static string MoveUp { + get { + return ResourceManager.GetString("MoveUp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Mr. /// @@ -2337,6 +2517,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to There are no collections to list.. + /// + public static string NoCollectionsToList { + get { + return ResourceManager.GetString("NoCollectionsToList", resourceCulture); + } + } + /// /// Looks up a localized string similar to There are no favorites in your vault.. /// @@ -2355,6 +2544,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to There are no folders to list.. + /// + public static string NoFoldersToList { + get { + return ResourceManager.GetString("NoFoldersToList", resourceCulture); + } + } + /// /// Looks up a localized string similar to There are no items in your vault.. /// @@ -2400,6 +2598,33 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to There are no items to list.. + /// + public static string NoItemsToList { + get { + return ResourceManager.GetString("NoItemsToList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No organizations to list.. + /// + public static string NoOrgsToList { + get { + return ResourceManager.GetString("NoOrgsToList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No passwords to list.. + /// + public static string NoPasswordsToList { + get { + return ResourceManager.GetString("NoPasswordsToList", resourceCulture); + } + } + /// /// Looks up a localized string similar to Notes. /// @@ -2445,6 +2670,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Number of Words. + /// + public static string NumberOfWords { + get { + return ResourceManager.GetString("NumberOfWords", resourceCulture); + } + } + /// /// Looks up a localized string similar to October. /// @@ -2508,6 +2742,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Ownership. + /// + public static string Ownership { + get { + return ResourceManager.GetString("Ownership", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passphrase. + /// + public static string Passphrase { + get { + return ResourceManager.GetString("Passphrase", resourceCulture); + } + } + /// /// Looks up a localized string similar to Passport Number. /// @@ -2535,6 +2787,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to This password has been exposed {0} time(s) in data breaches. You should change it.. + /// + public static string PasswordExposed { + get { + return ResourceManager.GetString("PasswordExposed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password generated.. /// @@ -2571,6 +2832,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Password History. + /// + public static string PasswordHistory { + get { + return ResourceManager.GetString("PasswordHistory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Are you sure you want to overwrite the current password?. /// @@ -2580,6 +2850,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use.. + /// + public static string PasswordSafe { + get { + return ResourceManager.GetString("PasswordSafe", resourceCulture); + } + } + /// /// Looks up a localized string similar to Phone. /// @@ -2598,6 +2877,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to PIN. + /// + public static string PIN { + get { + return ResourceManager.GetString("PIN", resourceCulture); + } + } + /// /// Looks up a localized string similar to Possible Matching Items. /// @@ -2743,7 +3031,34 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Search Vault. + /// Looks up a localized string similar to Search collection. + /// + public static string SearchCollection { + get { + return ResourceManager.GetString("SearchCollection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search folder. + /// + public static string SearchFolder { + get { + return ResourceManager.GetString("SearchFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search type. + /// + public static string SearchType { + get { + return ResourceManager.GetString("SearchType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search vault. /// public static string SearchVault { get { @@ -2751,6 +3066,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Secure Notes. + /// + public static string SecureNotes { + get { + return ResourceManager.GetString("SecureNotes", resourceCulture); + } + } + /// /// Looks up a localized string similar to Security. /// @@ -2787,6 +3111,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to You must select at least one collection.. + /// + public static string SelectOneCollection { + get { + return ResourceManager.GetString("SelectOneCollection", resourceCulture); + } + } + /// /// Looks up a localized string similar to What type of item do you want to add?. /// @@ -2859,6 +3192,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application.. + /// + public static string SetPINDescription { + get { + return ResourceManager.GetString("SetPINDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enter a 4 digit PIN code to unlock the app with.. /// @@ -2877,6 +3219,33 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Share. + /// + public static string Share { + get { + return ResourceManager.GetString("Share", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared.. + /// + public static string ShareDesc { + get { + return ResourceManager.GetString("ShareDesc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Share Item. + /// + public static string ShareItem { + get { + return ResourceManager.GetString("ShareItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Share Your Vault. /// @@ -2886,6 +3255,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Bitwarden allows you to share your vault with others by using an organization account. Would you like to visit the bitwarden.com website to learn more?. + /// + public static string ShareVaultConfirmation { + get { + return ResourceManager.GetString("ShareVaultConfirmation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Create an organization to securely share your items with other users.. /// @@ -3084,6 +3462,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Type. + /// + public static string Type { + get { + return ResourceManager.GetString("Type", resourceCulture); + } + } + /// /// Looks up a localized string similar to Card. /// @@ -3111,6 +3498,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Types. + /// + public static string Types { + get { + return ResourceManager.GetString("Types", resourceCulture); + } + } + /// /// Looks up a localized string similar to Secure Note. /// @@ -3138,6 +3534,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Unlock. + /// + public static string Unlock { + get { + return ResourceManager.GetString("Unlock", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unlock with {0}. /// @@ -3192,6 +3597,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to URIs. + /// + public static string URIs { + get { + return ResourceManager.GetString("URIs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use another two-step login method. /// @@ -3255,6 +3669,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Value. + /// + public static string Value { + get { + return ResourceManager.GetString("Value", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} has been copied.. /// @@ -3273,6 +3696,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Your vault is locked. Verify your master password to continue.. + /// + public static string VaultLockedMasterPassword { + get { + return ResourceManager.GetString("VaultLockedMasterPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your vault is locked. Verify your PIN code to continue.. + /// + public static string VaultLockedPIN { + get { + return ResourceManager.GetString("VaultLockedPIN", resourceCulture); + } + } + /// /// Looks up a localized string similar to Verification Code. /// @@ -3435,6 +3876,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Who owns this item?. + /// + public static string WhoOwnsThisItem { + get { + return ResourceManager.GetString("WhoOwnsThisItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows Hello. /// @@ -3444,6 +3894,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Word Separator. + /// + public static string WordSeparator { + get { + return ResourceManager.GetString("WordSeparator", resourceCulture); + } + } + /// /// Looks up a localized string similar to Yes. /// @@ -3462,6 +3921,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Your account's fingerprint phrase. + /// + public static string YourAccountsFingerprint { + get { + return ResourceManager.GetString("YourAccountsFingerprint", resourceCulture); + } + } + /// /// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device's USB port, then touch its button.. /// diff --git a/src/App/Resources/AppResources.bg.resx b/src/App/Resources/AppResources.bg.resx deleted file mode 100644 index 90b6eed5a..000000000 --- a/src/App/Resources/AppResources.bg.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Относно - - - Добавяне - Add/create a new entity (verb). - - - Добавяне на папка - - - Добавяне на елемент - The title for the add item page. - - - Възникна грешка. - Alert title when something goes wrong. - - - Назад - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Отказ - Cancel an operation. - - - Копиране - Copy some value to your clipboard. - - - Копиране на паролата - The button text that allows a user to copy the login's password to their clipboard. - - - Копиране на потребителското име - The button text that allows a user to copy the login's username to their clipboard. - - - Благодарности - Title for page that we use to give credit to resources that we use. - - - Изтриване - Delete an entity (verb). - - - Изтриване... - Message shown when interacting with the server - - - Наистина ли искате да го изтриете? Действието не може да бъде отменено. - Confirmation alert message when deleteing something. - - - Редактиране - - - Редактиране на папка - - - Електронна поща - Short label for an email address. - - - Електронна поща - Full label for a email address. - - - Пишете ни - - - Email us directly to get help or leave feedback. - - - Enter your PIN code. - - - Любими - Title for your favorite items in the vault. - - - File a Bug Report - - - Open an issue at our GitHub repository. - - - Use your fingerprint to verify. - - - Папка - Label for a folder. - - - New folder created. - - - Folder deleted. - - - Няма папка - Items that have no folder specified go in this special "catch-all" folder. - - - Папки - - - Folder updated. - - - Посещаване на сайта - The button text that allows user to launch the website to their web browser. - - - Помощ и обратна връзка - - - Скриване - Hide a secret value that is currently shown (password). - - - Please connect to the internet before continuing. - Description message for the alert when internet connection is required to continue. - - - Internet Connection Required - Title for the alert when internet connection is required to continue. - - - Invalid Master Password. Try again. - - - Invalid PIN. Try again. - - - Пускане - The button text that allows user to launch the website to their web browser. - - - Вписване - The login button text (verb). - - - Вход - Title for login page. (noun) - - - Отписване - The log out button text (verb). - - - Сигурни ли сте, че искате да се отпишете? - - - Главна парола - Label for a master password. - - - Още - Text to define that there are more options things to see. - - - Моят трезор - The title for the vault page. - - - Наименование - Label for an entity name. - - - Не - - - Бележки - Label for notes. - - - Добре - Acknowledgement. - - - Парола - Label for a password. - - - Запазване - Button text for a save operation (verb). - - - Запазване... - Message shown when interacting with the server - - - Настройки - The title for the settings page. - - - Показване - Reveal a hidden value (password). - - - Елементът беше изтрит. - Confirmation message after successfully deleting a login. - - - Подаване - - - Синхронизиране - The title for the sync page. - - - Благодарим ви! - - - Сечива - The title for the tools page. - - - Адрес - Label for a uri/url. - - - Use Fingerprint to Unlock - - - Потребителско име - Label for a username. - - - Полето {0} е задължително. - Validation message for when a form field is left blank and is required to be entered. - - - {0} беше копирано. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verify Fingerprint - - - Verify Master Password - - - Verify PIN - - - Версия - - - Преглед - - - Посетете нашия сайт - - - Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden. - - - Сайт - Label for a website. - - - Да - - - Сметка - - - Your new account has been created! You may now log in. - - - Добавяне на елемент - - - App Extension - - - Use the Bitwarden accessibility service to auto-fill your logins across apps and the web. - - - Auto-fill Service - - - Avoid Ambiguous Characters - - - Bitwarden App Extension - - - The easiest way to add new logins to your vault is from the Bitwarden App Extension. Learn more about using the Bitwarden App Extension by navigating to the "Tools" screen. - - - Use Bitwarden in Safari and other apps to auto-fill your logins. - - - Bitwarden Auto-fill Service - - - Use the Bitwarden accessibility service to auto-fill your logins. - - - Промяна на имейла - - - You can change your email address on the bitwarden.com web vault. Do you want to visit the website now? - - - Change Master Password - - - You can change your master password on the bitwarden.com web vault. Do you want to visit the website now? - - - Затваряне - - - Очаквайте скоро! - - - Продължаване - - - Copied! - - - Copied password! - - - Copied username! - - - Създаване на сметка - - - Създаване на сметката... - Message shown when interacting with the server - - - Редактиране на елемента - - - Enable Automatic Syncing - - - Enter your account email address to receive your master password hint. - - - Re-enable App Extension - - - Почти готово! - - - Enable App Extension - - - In Safari, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu). - Safari is the name of apple's web browser - - - Get instant access to your passwords! - - - You're ready to log in! - - - See Supported Apps - - - Your logins are now easily accessable from Safari, Chrome, and other supported apps. - - - In Safari and Chrome, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the share menu). - - - Tap the Bitwarden icon in the menu to launch the extension. - - - To turn on Bitwarden in Safari and other apps, tap the "more" icon on the bottom row of the menu. - - - Favorite - - - Fingerprint - - - Генериране на парола - - - Get your master password hint - - - Import Items - - - You can bulk import items from the bitwarden.com web vault. Do you want to visit the website now? - - - Quickly bulk import your items from other password management apps. - - - Последно синхронизиране: - - - Дължина - - - Заключване - - - 15 минути - - - 1 час - - - 1 минута - - - 4 часа - - - Незабавно - - - Lock Options - - - Вписване... - Message shown when interacting with the server - - - Log in or create a new account to access your secure vault. - - - Управление - - - Password confirmation is not correct. - - - The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it. - - - Master Password Hint (optional) - - - A master password hint can help you remember your password if you forget it. - - - Master password must be at least 8 characters long. - - - Минимален брой цифри - Minimum numeric characters for password generator settings - - - Минимален брой специални знаци - Minimum special characters for password generator settings - - - Допълнителни настройки - - - You must log into the main Bitwarden app before you can use the extension. - - - Никога - - - New item created. - - - There are no favorites in your vault. - - - There are no items in your vault. - - - There are no items in your vault for this website. Tap to add one. - - - This login does not have a username or password configured. - - - Добре, разбрах! - Confirmation, like "Ok, I understand it" - - - Option defaults are set from the main Bitwarden app's password generator tool. - - - Настройки - - - Други - - - Password generated. - - - Password Generator - - - Подсказка за паролата - - - We've sent you an email with your master password hint. - - - Are you sure you want to overwrite the current password? - - - Bitwarden keeps your vault automatically synced by using push notifications. For the best possible experience, please select "Allow" on the following prompt when asked to enable push notifications. - Push notifications for apple products - - - Rate the App - - - Please consider helping us out with a good review! - - - App Store ratings are reset with every new version of Bitwarden. Please consider helping us out with a good review! - - - Regenerate Password - - - Re-type Master Password - - - Search Vault - - - Security - - - See Development Progress - - - Select - - - Set PIN - - - Enter a 4 digit PIN code to unlock the app with. - - - Сведения за елемента - - - Item updated. - - - Подаване... - Message shown when interacting with the server - - - Синхронизиране... - Message shown when interacting with the server - - - Синхронизирането завърши. - - - Синхронизирането е неуспешно. - - - Синхронизиране сега - - - Touch ID - What Apple calls their fingerprint reader. - - - Two-step Login - - - Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now? - - - Unlock with {0} - - - Unlock with PIN Code - - - Validating - Message shown when interacting with the server - - - Verification Code - - - Преглед на елемента - - - Bitwarden Web Vault - - - Manage your items from any web browser with the Bitwarden web vault. - - - Lost authenticator app? - - - Елементи - Screen title - - - Разширението е включено! - - - Иконки - - - Преводи - - - Items for {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - There are no items in your vault for {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - When you see a Bitwarden auto-fill notification, you can tap it to launch the auto-fill service. - - - Tap this notification to auto-fill an item from your vault. - - - Open Accessibility Settings - - - 1. On the Android Accessibility Settings screen, touch "Bitwarden" under the Services heading. - - - 2. Switch on the toggle and press OK to accept. - - - Изключено - - - Включено - - - Състояние - - - Бета - - - The easiest way to add new logins to your vault is from the Bitwarden Auto-fill Service. Learn more about using the Bitwarden Auto-fill Service by navigating to the "Tools" screen. - - - Автоматично попълване - - - Do you want to auto-fill or view this item? - - - Are you sure you want to auto-fill this item? It is not a complete match for "{0}". - - - Matching Items - - - Possible Matching Items - - - Търсене - - - You are searching for an auto-fill item for "{0}". - - - Share Your Vault - - - Create an organization to securely share your items with other users. - - - Scan When Password Field Focused - - - Only scan the screen for fields and offer an auto-fill notification whenever you select a password field. This setting may help conserve battery life. - - - Persist Notification - - - Always offer an auto-fill notification and only scan for fields after attempting an auto-fill. This setting may help conserve battery life. - - - Always Scan - - - Always scan the screen for fields and only offer an auto-fill notification if password fields are found. This is the default setting. - - - Cannot open the app "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Authenticator App - For 2FA - - - Enter the 6 digit verification code from your authenticator app. - For 2FA - - - Enter the 6 digit verification code that was emailed to {0}. - For 2FA - - - Login Unavailable - For 2FA whenever there are no available providers on this device. - - - This account has two-step login enabled, however, none of the configured two-step providers are supported on this device. Please use a supported device and/or add additional providers that are better supported across devices (such as an authenticator app). - - - Recovery Code - For 2FA - - - Remember me - Remember my two-step login - - - Send verification code email again - For 2FA - - - Two-step Login Options - - - Use another two-step login method - - - Could not send verification email. Try again. - For 2FA - - - Verification email sent. - For 2FA - - - To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device's USB port, then touch its button. - - - YubiKey Security Key - "YubiKey" is the product name and should not be translated. - - - Прикачване на файл - - - Прикачени файлове - - - Unable to download file. - - - Your device cannot open this type of file. - - - Изтегляне... - Message shown when downloading a file - - - This attachment is {0} in size. Are you sure you want to download it onto your device? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Authenticator Key (TOTP) - - - Verification Code (TOTP) - Totp code label - - - Authenticator key added. - - - Cannot read authenticator key. - - - Scanning will happen automatically. - - - Point your camera at the QR code. - - - Scan QR Code - - - Фотоапарат - - - Снимки - - - Copied TOTP! - - - Copy TOTP - - - If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login. - - - Disable Automatic TOTP Copy - - - A premium membership is required to use this feature. - - - Attachment added - - - Attachment deleted - - - Изберете файл - - - Файл - - - No file chosen - - - There are no attachments. - - - File Source - - - Feature Unavailable - - - Maximum file size is 100 MB. - - - You cannot use this feature until you update your encryption key. - - - Learn More - - - API Server URL - - - Custom Environment - - - For advanced users. You can specify the base URL of each service independently. - - - The environment URLs have been saved. - - - {0} is not correctly formatted. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identity Server URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Self-hosted Environment - - - Specify the base URL of your on-premise hosted Bitwarden installation. - - - Server URL - - - Web Vault Server URL - - - Tap this notification to view items from your vault. - - - Custom Fields - - - Copy Number - - - Copy Security Code - - - Номер - - - Код за сигурност - - - What type of item do you want to add? - - - Карта - - - Самоличност - - - Запис - - - Secure Note - - - Адрес 1 - - - Адрес 2 - - - Адрес 3 - - - April - - - August - - - Марка - - - Име на притежателя на картата - - - Град / село - - - Фирма - - - Държава - - - Декември - - - Д-р - - - Месец на изтичане - - - Година на изтичане - - - Февруари - - - Собствено име - - - Януари - - - Юли - - - Юни - - - Фамилно име - - - Номер на лиценза - - - Март - - - Май - - - Презиме - - - Г-н - - - Г-жа - - - Г-ца - - - Ноември - - - Октомври - - - Номер на паспорта - - - Телефон - - - Септември - - - Номер на осигуровката - - - Област - - - Обръщение - - - Пощенски код - - - Адрес - - - Изтичане - - - Изключване на иконките на сайтовете - - - Иконките на сайтовете са разпознаваемо изображение за всеки запис в трезора. - - - Адрес на сървъра с иконки - - - Auto-fill with Bitwarden - - - Трезорът е заключен - - - Go to my vault - - - Колекции - - - There are no items in this collection. - - - There are no items in this folder. - - - Auto-fill Accessibility Service - - - The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. - - - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. - - - Open Autofill Settings - - - Face ID - What Apple calls their facial recognition reader. - - - Use Face ID to verify. - - - Use Face ID To Unlock - - - Verify Face ID - - - Unlock with Windows Hello - - - Verify with Windows Hello - - - Windows Hello - - - We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings > System > Languages and input > Advanced > Autofill service. - - - Custom Field Name - - - Boolean - - - Hidden - - - Text - - - New Custom Field - - - What type of custom field do you want to add? - - - Remove - - - New URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Base domain - - - Default - - - Exact - - - Сървър - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Регулярен израз - A programming term, also known as 'RegEx'. - - - Започва с - - - Откриване на съвпадения в адрес - - - Откриване на съвпадения - URI match detection for auto-fill. - - - Yes, and Save - - - Auto-fill and save - - - Organization - An entity of multiple related people (ex. a team or business organization). - - - Hold your Yubikey near the top of the device. - - - Try Again - - - To continue, hold your YubiKey NEO against the back of the device. - - - The accessibility service may be helpful to use when apps do not support the standard auto-fill service. - - - Password Updated - ex. Date this password was updated - - - Updated - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.cs.Designer.cs b/src/App/Resources/AppResources.cs.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.cs.resx b/src/App/Resources/AppResources.cs.resx deleted file mode 100644 index 1f62f34ed..000000000 --- a/src/App/Resources/AppResources.cs.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - O aplikaci - - - Přidat - Add/create a new entity (verb). - - - Přidat složku - - - Přidat položku - The title for the add item page. - - - Došlo k chybě. - Alert title when something goes wrong. - - - Zpět - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Zrušit - Cancel an operation. - - - Kopírovat - Copy some value to your clipboard. - - - Kopírovat heslo - The button text that allows a user to copy the login's password to their clipboard. - - - Kopírovat uživatelské jméno - The button text that allows a user to copy the login's username to their clipboard. - - - Autoři - Title for page that we use to give credit to resources that we use. - - - Smazat - Delete an entity (verb). - - - Mazání... - Message shown when interacting with the server - - - Opravdu chcete smazat tuto položku? Akce je nevratná. - Confirmation alert message when deleteing something. - - - Upravit - - - Upravit složku - - - E-mail - Short label for an email address. - - - E-mailová adresa - Full label for a email address. - - - Napište nám - - - Napište nám e-mail pro získání pomoci nebo zanechání zpětné vazby. - - - Zadejte PIN kód. - - - Oblíbené - Title for your favorite items in the vault. - - - Nahlásit chybu - - - Nahlaste chybu přímo v našem GitHub repositáři. - - - Pro ověření použijte otisk prstu. - - - Složka - Label for a folder. - - - Složka byla přidána - - - Složka byla smazána - - - Žádná složka - Items that have no folder specified go in this special "catch-all" folder. - - - Složky - - - Složka byla upravena - - - Otevřít webovou stránku - The button text that allows user to launch the website to their web browser. - - - Nápověda a zpětná vazba - - - Skrýt - Hide a secret value that is currently shown (password). - - - Prosím připojte se k internetu. - Description message for the alert when internet connection is required to continue. - - - Je nutné připojení k internetu - Title for the alert when internet connection is required to continue. - - - Chybné hlavní heslo. Zkuste to znovu. - - - Chybný PIN. Zkuste to znovu. - - - Spustit - The button text that allows user to launch the website to their web browser. - - - Přihlásit se - The login button text (verb). - - - Přihlašovací údaje - Title for login page. (noun) - - - Odhlásit se - The log out button text (verb). - - - Opravdu se chcete odhlásit? - - - Hlavní heslo - Label for a master password. - - - Více - Text to define that there are more options things to see. - - - Můj trezor - The title for the vault page. - - - Název - Label for an entity name. - - - Ne - - - Poznámky - Label for notes. - - - Ok - Acknowledgement. - - - Heslo - Label for a password. - - - Uložit - Button text for a save operation (verb). - - - Ukládání... - Message shown when interacting with the server - - - Nastavení - The title for the settings page. - - - Zobrazit - Reveal a hidden value (password). - - - Položka byla smazána - Confirmation message after successfully deleting a login. - - - Potvrdit - - - Synchronizace - The title for the sync page. - - - Poděkování - - - Nástroje - The title for the tools page. - - - URI - Label for a uri/url. - - - Odemknout otiskem prstu - - - Uživatelské jméno - Label for a username. - - - Pole {0} je povinné. - Validation message for when a form field is left blank and is required to be entered. - - - {0} bylo zkopírováno. - Confirmation message after suceessfully copying a value to the clipboard. - - - Ověření otisku prstu - - - Ověření hlavního hesla - - - Ověřit PIN - - - Verze - - - Zobrazit - - - Navštivit naší webovou stránku - - - Navštivte náš web pro získání nápovědy, novinek, kontaktu, nebo návodů, jak používat Bitwarden. - - - Webová stránka - Label for a website. - - - Ano - - - Účet - - - Váš účet byl vytvořen! Můžete se přihlásit. - - - Přidat položku - - - Rozšíření aplikace - - - Služba přístupnosti Bitwarden umožňuje automatické vyplňování přihlašovacích údajů v aplikacích a na webových stránkách. - - - Služba automatického vyplňování - - - Nepoužít zaměnitelné znaky - - - Rozšíření aplikace Bitwarden - - - Nejjednodušší způsob, jak přidat nové přihlašovací údaje do vašeho trezoru, je použití rozšíření aplikace Bitwarden. Další informace o použití tohoto rozšíření naleznete na obrazovce „Nástroje“. - - - Použijte Bitwarden v Safari a ostatních prohlížečích pro automatické vyplnění přihlašovacích údajů. - - - Služba automatického vyplňování - - - Použijte službu přístupnosti pro automatické vyplňování přihlašovacích údajů. - - - Změnit e-mail - - - E-mailovou adresu si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít? - - - Změnit hlavní heslo - - - Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít? - - - Zavřít - - - Již brzy! - - - Pokračovat - - - Zkopírováno! - - - Heslo bylo úspěšně zkopírováno! - - - Uživatelské jméno bylo úspěšně zkopírováno! - - - Vytvořit účet - - - Vytváření účtu... - Message shown when interacting with the server - - - Upravit položku - - - Povolit automatickou synchronizaci - - - Zadejte e-mailovou adresu pro zaslání nápovědy k hlavnímu heslu. - - - Znovu zapnout rozšíření aplikace - - - Téměř hotovo! - - - Zapnout rozšíření aplikace - - - V Safari používá Bitwarden ikonku pro sdílení (nápověda: nachází se vpravo dole v menu). - Safari is the name of apple's web browser - - - Získejte okamžitý přístup ke svým heslům! - - - Jste připraveni se přihlásit! - - - Zobrazit podporované aplikace - - - Vaše přihlašovací údaje jsou nyní snadno přístupné ze Safari, Chromu a dalších podporovaných aplikací. - - - V Safari a Chromu najdete Bitwarden pomocí ikony sdílení (nápověda: nachází se vpravo dole v menu). - - - Pro spuštění rozšíření klikněte na ikonku Bitwarden v menu. - - - Pro zapnutí Bitwarden v prohlížeči Safari a dalších aplikacích klepněte na ikonku „více“ v dolní části menu. - - - Oblíbené - - - Otisk prstu - - - Vygenerovat heslo - - - Zaslat nápovědu k hlavnímu heslu - - - Importovat položky - - - Můžete hromadně importovat položky do vašeho trezoru na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít? - - - Importujte rychle a hromadně údaje z jiných aplikací pro správu hesel. - - - Poslední synchronizace: - - - Délka - - - Zamknout - - - Po 15 minutách - - - Po 1 hodině - - - Po 1 minutě - - - Po 4 hodinách - - - Okamžitě - - - Možnosti zamknutí - - - Přihlašování... - Message shown when interacting with the server - - - Pro přístup do vašeho bezpečného trezoru se přihlašte nebo si vytvořte nový účet. - - - Správa - - - Potvrzení hesla se neshoduje. - - - Hlavní heslo je heslo, které používáte k přístupu do vašeho trezoru. Je velmi důležité, abyste jej nezapomněli. Neexistuje totiž žádný způsob, jak heslo obnovit v případě, že jste na něj zapomněli. - - - Nápověda k hlavnímu heslu (volitelné) - - - Nápověda k hlavnímu heslu vám pomůže zapamatovat si heslo, pokud ho zapomenete. - - - Hlavní heslo musí obsahovat alespoň 8 znaků. - - - Minimální počet čísel - Minimum numeric characters for password generator settings - - - Minimální počet speciálních znaků - Minimum special characters for password generator settings - - - Další nastavení - - - Před použitím rozšíření se musíte nejdříve přihlásit v hlavní aplikaci Bitwarden. - - - Nikdy - - - Položka byla přidána - - - Žádné oblíbené přihlašovací údaje. - - - Žádné položky. - - - Pro tuto stránku neexistují žádné položky. Klikněte pro jejich přidání. - - - Tyto přihlašovací údaje nemají nastavené uživatelské jméno nebo heslo. - - - OK, rozumím! - Confirmation, like "Ok, I understand it" - - - Výchozí nastavení je převzato z nastavení generování hesel v aplikaci Bitwarden. - - - Možnosti - - - Ostatní - - - Heslo bylo vygenerováno. - - - Generátor hesla - - - Nápověda k heslu - - - Odeslali jsme vám e-mail s nápovědou k hlavnímu heslu. - - - Opravdu chcete přepsat aktuální heslo? - - - Bitwarden udržuje váš trezor automaticky synchronizovaný pomocí nabízených oznámení. Pro nejlepší možný zážitek klikněte prosím na následující výzvě na "Ok", pokud budete požádáni o jejich povolení. - Push notifications for apple products - - - Ohodnotit aplikaci - - - Pomozte nám napsáním dobré recenze! - - - S každou novou verzí aplikace Bitwarden se v App Store obnovuje hodnocení. Pomozte nám prosím dobrou recenzí! - - - Vygenerovat další heslo - - - Znovu zadejte hlavní heslo - - - Vyhledat v trezoru - - - Zabezpečení - - - Viz vývoj aplikace - - - Vybrat - - - Nastavit PIN - - - Zadejte 4-místný PIN kód pro odemknutí aplikace. - - - Informace o položce - - - Položka byla upravena - - - Odesílání... - Message shown when interacting with the server - - - Synchronizování... - Message shown when interacting with the server - - - Synchronizace je dokončena. - - - Synchronizace selhala. - - - Synchronizovat nyní - - - Touch ID - What Apple calls their fingerprint reader. - - - Dvoufázové přihlášení - - - Dvoufázové přihlášení činí váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z aplikace, SMS, e-mailu nebo telefonního hovoru. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít? - - - Odemknout pomocí {0} - - - Odemknout pomocí PIN kódu - - - Ověřování - Message shown when interacting with the server - - - Ověřovací kód - - - Zobrazit položku - - - Webová aplikace - - - Spravujte přihlašovací údaje z jakéhokoliv prohlížeče pomocí webové aplikace Bitwarden. - - - Ztratili jste ověřovací aplikaci? - - - Položky - Screen title - - - Rozšíření aktivováno! - - - Ikony - - - Preklady - - - Položky pro {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Žádné položky pro {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Pokud uvidíte v liště oznámení z aplikace Bitwarden, můžete klepnutím na něj spustit automatické vyplnění přihlašovacích údajů. - - - Klikněte pro automatické vyplnění přihlašovacích údajů. - - - Otevřít nastavení přístupnosti - - - 1. V nastavení Přístupnosti systému Android klepněte na „Bitwarden“ v části „Stažené služby“. - - - 2. Zapněte přepínač a potvrďte stisknutím tlačítka OK. - - - Zakázáno - - - Povoleno - - - Stav - - - Beta - - - Nejjednodušší způsob, jak přidat nové přihlašovací údaje do vašeho trezoru, je služba automatického vyplňování Bitwarden. Další informace o použití této služby naleznete na obrazovce „Nástroje“. - - - Automatické vyplnění - - - Chcete automaticky vyplnit nebo zobrazit tyto přihlašovací údaje? - - - Opravdu chcete tyto přihlašovací údaje automaticky vyplnit? Nejsou úplně shodné s "{0}". - - - Odpovídající položky - - - Možné odpovídající položky - - - Hledat - - - Hledáte přihlašovací údaje k automatickému vyplnění pro "{0}". - - - Sdílet trezor - - - Vytvořte organizaci pro bezpečné sdílení přihlašovacích údajů s dalšími uživateli. - - - Prohledávat jen při vybrání pole hesla - - - Prohledávat pole obrazovky pouze při výběru pole s heslem a zobrazit oznámení automatického vyplnění údajů. Toto nastavení může pomoci prodloužit výdrž baterie. - - - Vždy zobrazovat oznámení - - - Vždy zobrazovat oznámení automatického vyplnění a pouze po kliknutí na něj prohledávat pole obrazovky. Toto nastavení může pomoci prodloužit výdrž baterie. - - - Vždy prohledávat - - - Vždy prohledávat pole obrazovky a pouze v případě nalezení pole s heslem zobrazit oznámení automatického vyplnění. Toto je výchozí nastavení. - - - Nelze otevřít aplikaci "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Ověřovací aplikace - For 2FA - - - Zadejte 6-místný kód z ověřovací aplikace. - For 2FA - - - Zadejte 6místný kód z e-mailu, který byl zaslán na {0}. - For 2FA - - - Přihlášení není dostupné - For 2FA whenever there are no available providers on this device. - - - Tento účet má zapnuté dvoufázové ověřování, ale žádný z nastavených poskytovalů dvoufázového přihlášení není na tomto zařízení podporován. Použijte prosím podporované zařízení a přidejte další poskytovatele, který je podporován na více různých zařízeních (například ověřovací aplikace). - - - Kód pro obnovení - For 2FA - - - Pamatovat přihlášení - Remember my two-step login - - - Znovu zaslat ověřovací kód na e-mail - For 2FA - - - Možnosti dvoufázového přihlášení - - - Použít jinou metodu dvoufázového přihlášení - - - Ověřovací e-mail se nepodařilo odeslat. Zkuste to znovu. - For 2FA - - - Ověřovací e-mail byl odeslán. - For 2FA - - - Pro pokračování podržte YubiKey NEO blízko zadní strany vašeho zařízení nebo vložte YubiKey do USB portu a stiskněte tlačítko na klíči. - - - YubiKey NEO bezpečnostní klíč - "YubiKey" is the product name and should not be translated. - - - Přidat přílohu - - - Přílohy - - - Soubor se nepodařilo stáhnout. - - - Vaše zařízení nemůže otevřít tento typ souboru. - - - Stahování... - Message shown when downloading a file - - - Tato příloha je velká {0}. Opravdu jí chcete stáhnout do svého zařízení? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Autentizační klíč (TOTP) - - - Ověřovací kód (TOTP) - Totp code label - - - Autentizační klíč byl přidán - - - Nelze přečíst ověřovací klíč. - - - Načtení proběhne automaticky. - - - Namiřte fotoaparát na QR kód. - - - Načíst QR kód - - - Fotoaparát - - - Fotografie - - - Ověřovací kód (TOTP) byl zkopírován! - - - Zkopírovat ověřovací kód (TOTP) - - - Pokud mají vaše přihlašovací údaje přidán autentizační klíč pro TOTP, vygenerovaný ověřovací kód (TOTP) se automaticky zkopíruje do schránky při každém automatickém vyplnění přihlašovacích údajů. - - - Zakázat automatické kopírování TOTP kódu - - - Pro použití této funkce je potřebné prémiové členství. - - - Příloha byla přidána - - - Příloha byla smazána - - - Vybrat soubor - - - Soubor - - - Není vybrán žádný soubor - - - Žádné přílohy. - - - Vybrat soubor - - - Funkce není dostupná - - - Maximální velikost souboru je 100 MB. - - - Tuto funkci nemůžete použít dokud neaktualizujete svůj šifrovací klíč. - - - Dozvědět se více - - - URL serveru API - - - Vlastní prostředí - - - Pro pokročilé uživatele. Můžete zadat základní URL adresu každé služby zvlášť. - - - URL adresy vlastního prostředí byly uloženy - - - {0} má nesprávný formát. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL serveru identity - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Vlastnoručně hostované prostředí - - - Zadejte základní URL adresu vlastnoručně hostované aplikace Bitwarden. - - - URL serveru - - - URL serveru webového trezoru - - - Klepnutím na toto oznámení zobrazíte přihlašovací údaje z trezoru. - - - Vlastní pole - - - Kopírovat číslo - - - Kopírovat bezpečnostní kód - - - Číslo - - - Bezpečnostní kód - - - Jaký typ položky chcete přidat? - - - Kreditní karta - - - Identita - - - Přihlášení - - - Poznámka - - - Adresa 1 - - - Adresa 2 - - - Adresa 3 - - - Duben - - - Srpen - - - Značka - - - Jméno držitele karty - - - Město - - - Firma - - - Stát - - - Prosinec - - - MUDr - - - Měsíc expirace - - - Rok expirace - - - Únor - - - Jméno - - - Leden - - - Červenec - - - Červen - - - Příjmení - - - Číslo dokladu totožnosti - - - Březen - - - Květen - - - Druhé jméno - - - Pan - - - Paní - - - Slečna - - - Listopad - - - Říjen - - - Číslo cestovního pasu - - - Telefon - - - Září - - - Číslo sociálního pojištění - - - Kraj / Provincie - - - Oslovení - - - PSČ - - - Adresa - - - Expirace - - - Zakázat ikonky webových stránek - - - Ikonky webových stránek zobrazí snadno rozeznatelný obrázek vedle každé položky ve vašem trezoru. - - - URL serveru ikonek - - - Automatické vyplnění s aplikací Bitwarden - - - Trezor je zamknutý - - - Otevřít trezor - - - Kolekce - - - V této kolekci nejsou žádné položky. - - - V této složce nejsou žádné položky. - - - Služba přístupnosti - - - Služba Bitwarden automatického vyplnění používá Android Autofill framework pro vyplnění přihlašovacích údajů, čísel platebních karet a dalších osobních informací do dalších aplikací ve vašem zařízení. - - - Použijte službu přístupnosti pro automatické vyplňování přihlašovacích údajů, platebních karet či identit v ostatních aplikacích. - - - Otevřít nastavení automatického vyplňování - - - Face ID - What Apple calls their facial recognition reader. - - - Použít Face ID k ověření. - - - Použít Face ID k odemknutí - - - Ověření Face ID - - - Odemknout pomocí Windows Hello - - - Ověřit pomocí Windows Hello - - - Windows Hello - - - Obrazovku nastavení automatického vyplňování Android se nepodařilo otevřít. Nastavení můžete otevřít ručně z Nastavení systému Android > Systém > Jazyky a zadávání > Rozšířená nastavení > Služba automatického vyplňování. - - - Název vlastního pole - - - Ano/Ne - - - Skryté - - - Text - - - Nové vlastní pole - - - Jaký typ vlastního pole chcete přidat? - - - Smazat - - - Nová URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Základní doména - - - Výchozí - - - Přesně - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regulární výraz - A programming term, also known as 'RegEx'. - - - Začíná na - - - Zjišťování shody URI - - - Zjišťování shody - URI match detection for auto-fill. - - - Ano a uložit - - - Automaticky vyplněno a uloženo. - - - Organizace - An entity of multiple related people (ex. a team or business organization). - - - Držte Yubikey v horní části zařízení. - - - Zkusit znovu - - - Pro pokračování přidržte YubiKey NEO u zadní části zařízení. - - - Služba přístupnosti může být užitečná, pokud aplikace nepodporují standardní službu automatického vyplňování. - - - Heslo bylo změněno - ex. Date this password was updated - - - Změněno - ex. Date this item was updated - - - Automatické vyplňování zapnuto. - - - Před použitím automatického vyplňování se musíte nejdříve přihlásit v hlavní aplikaci Bitwarden. - - - Vaše přihlašovací údaje jsou nyní snadno přístupné přímo z vaší klávesnice během přihlašování do aplikací či webů. - - - Doporučujeme vypnout v nastavení všechny další aplikace pro automatické vyplňování údajů, pokud je neplánujete používat. - - - Přistupujte k vašemu trezoru přímo z vaší klávesnice pro rychlejší automatické vyplnění hesel. - - - Pokyny pro zapnutí automatického vyplňování hesel na vašem zařízení: - - - 1. Přejděte do aplikace „Nastavení“ v iOS - - - 2. Klepněte na „Hesla a účty“ - - - 3. Klepněte na „Automatické vyplňování hesel“ - - - 4. Zapněte automatické vyplňování - - - 5. Vyberte „Bitwarden“ - - - Automatické vyplnění hesel - - - Nejjednodušší způsob, jak přidat nové přihlašovací údaje do vašeho trezoru, je rozšíření automatického vyplňování Bitwarden. Další informace o použití tohoto rozšíření naleznete na obrazovce „Nástroje“. - - diff --git a/src/App/Resources/AppResources.da.Designer.cs b/src/App/Resources/AppResources.da.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.da.resx b/src/App/Resources/AppResources.da.resx deleted file mode 100644 index 1a3c7f7aa..000000000 --- a/src/App/Resources/AppResources.da.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Om - - - Tilføj - Add/create a new entity (verb). - - - Tilføj mappe - - - Tilføj element - The title for the add item page. - - - Der opstod en fejl. - Alert title when something goes wrong. - - - Tilbage - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Annullér - Cancel an operation. - - - Kopiér - Copy some value to your clipboard. - - - Kopiér adgangskode - The button text that allows a user to copy the login's password to their clipboard. - - - Kopiér brugernavn - The button text that allows a user to copy the login's username to their clipboard. - - - Tak til - Title for page that we use to give credit to resources that we use. - - - Slet - Delete an entity (verb). - - - Sletter... - Message shown when interacting with the server - - - Vil du virkelig slette? Dette kan ikke fortrydes. - Confirmation alert message when deleteing something. - - - Redigér - - - Redigér mappe - - - E-mail - Short label for an email address. - - - E-mailadresse - Full label for a email address. - - - E-mail os - - - E-mail os direkte for at få hjælp eller give feedback. - - - Indtast din PIN-kode. - - - Favoritter - Title for your favorite items in the vault. - - - Indsend en fejlrapport - - - Opret en supportsag i vores GitHub-arkiv. - - - Verificér med dit fingeraftryk. - - - Mappe - Label for a folder. - - - Ny mappe oprettet. - - - Mappe slettet. - - - Ingen mappe - Items that have no folder specified go in this special "catch-all" folder. - - - Mapper - - - Mappe opdateret. - - - Gå til hjemmeside - The button text that allows user to launch the website to their web browser. - - - Hjælp og feedback - - - Skjul - Hide a secret value that is currently shown (password). - - - Opret forbindelse til internettet inden du fortsætter. - Description message for the alert when internet connection is required to continue. - - - Internetforbindelse kræves - Title for the alert when internet connection is required to continue. - - - Ugyldig hovedadgangskode. Prøv igen. - - - Ugyldig PIN-kode. Prøv igen. - - - Gå til - The button text that allows user to launch the website to their web browser. - - - Log ind - The login button text (verb). - - - Login - Title for login page. (noun) - - - Log ud - The log out button text (verb). - - - Er du sikker på, at du vil logge ud? - - - Hovedadgangskode - Label for a master password. - - - Flere - Text to define that there are more options things to see. - - - Min boks - The title for the vault page. - - - Navn - Label for an entity name. - - - Nej - - - Noter - Label for notes. - - - Ok - Acknowledgement. - - - Adgangskode - Label for a password. - - - Gem - Button text for a save operation (verb). - - - Gemmer... - Message shown when interacting with the server - - - Indstillinger - The title for the settings page. - - - Vis - Reveal a hidden value (password). - - - Element er blevet slettet. - Confirmation message after successfully deleting a login. - - - Indsend - - - Synkronisér - The title for the sync page. - - - Tak - - - Værktøjer - The title for the tools page. - - - URI - Label for a uri/url. - - - Benyt fingeraftryk til oplåsning - - - Brugernavn - Label for a username. - - - Feltet {0} er obligatorisk. - Validation message for when a form field is left blank and is required to be entered. - - - {0} er blevet kopieret. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verificér fingeraftryk - - - Verificér hovedadgangskode - - - Bekræft PIN-kode - - - Version - - - Vis - - - Besøg vores hjemmeside - - - Besøg vores hjemmeside for at få hjælp, nyheder, e-maile os og/eller få mere at vide om, hvordan du benytter Bitwarden. - - - Hjemmeside - Label for a website. - - - Ja - - - Konto - - - Din nye konto er oprettet! Du kan nu logge ind. - - - Tilføj et element - - - App-udvidelse - - - Brug Bitwardens hjælpefunktion til autoudfyldning af dine logins på tværs af apps og nettet. - - - Autoudfyldningstjeneste - - - Undgå tvetydige tegn - - - Bitwarden app-udvidelse - - - Den letteste måde at tilføje nye logins til din boks er fra Bitwarden app-udvidelsen. Få mere at vide om brugen af Bitwarden app-udvidelsen ved at gå til skærmen "Værktøjer". - - - Benyt Bitwarden i Safari og andre apps til autoudfyldelse af dine logins. - - - Bitwarden autoudfyldningstjeneste - - - Benyt Bitwardens hjælpefunktion til autoudfyldning af dine logins. - - - Skift e-mail - - - Du kan skifte din e-mailadresse i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu? - - - Skift hovedadgangskode - - - Du kan skifte din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu? - - - Luk - - - Kommer snart! - - - Forsæt - - - Kopieret! - - - Adgangskode kopieret! - - - Brugernavn kopieret! - - - Opret konto - - - Opretter konto... - Message shown when interacting with the server - - - Redigér element - - - Aktivér automatisk synkronisering - - - Angiv din kontos e-mailadresse for at modtage dit hovedadgangskodetip. - - - Genaktivér app-udvidelse - - - Næsten færdig! - - - Aktivér app-udvidelse - - - I Safari, find Bitwarden ved hjælp af delingsikonet (tip: Rul til højre på den nederste række i menuen). - Safari is the name of apple's web browser - - - Få øjeblikkelig adgang til dine adgangskoder! - - - Du er klar til at logge ind! - - - Se understøttede apps - - - Dine logins er nu lettilgængelige fra Safari, Chrome og andre understøttede apps. - - - I Safari og Chrome, find Bitwarden ved hjælp af delingsikonet (tip: Rul til højre på den nederste række i delingsmenuen). - - - Tryk på Bitwarden-ikonet i menuen for at starte udvidelsen. - - - For at aktivere Bitwarden i Safari og andre apps trykkes på "mere"-ikonet på den nederste række i menuen. - - - Favorit - - - fingeraftryk - - - Generér adgangskode - - - Få dit hovedadgangskodetip - - - Importér elementer - - - Du kan masseimportere elementer i din web-boks på bitwarden.com. Vil du besøge hjemmesiden nu? - - - Masseimportér hurtigt dine elementer fra andre adgangskodehåndterings-apps. - - - Seneste synkronisering: - - - Længde - - - Lås - - - 15 minutter - - - 1 time - - - 1 minut - - - 4 timer - - - Straks - - - Låseindstillinger - - - Logger ind... - Message shown when interacting with the server - - - Log ind, eller opret en ny konto, for at tilgå din sikre boks. - - - Håndtér - - - Adgangskodebekræftelse er forkert. - - - Hovedadgangskoden er den adgangskode, du benytter for at tilgå din boks. Det er meget vigtigt, at du ikke glemmer din hovedadgangskode. Der er ingen måde, hvorpå koden kan genoprettes, dersom du glemmer den. - - - Hovedadgangskodetip (valgfrit) - - - Et hovedeadgangskodentip kan hjælpe dig med at huske din adgangskode, hvis du glemmer den. - - - Hovedadgangskode skal være på minimum 8 tegn. - - - Minimum cifre - Minimum numeric characters for password generator settings - - - Minimum specialtegn - Minimum special characters for password generator settings - - - Flere indstillinger - - - Du skal logge ind på den primære Bitwarden-app, før du kan benytte udvidelsen. - - - Aldrig - - - Nyt element oprettet. - - - Der er ingen favoritter i din boks. - - - Der er ingen elementer i din boks. - - - Der er ingen elementer til denne hjemmeside/app i din boks. Tryk for at tilføje et. - - - Dette login har intet brugernavn eller adgangskode opsat. - - - Ok, forstået! - Confirmation, like "Ok, I understand it" - - - Standardindstillinger opsættes fra Bitwarden-app'ens adgangskodegeneratorværktøj. - - - Indstillinger - - - Andre - - - Adgangskode genereret. - - - Adgangskodegenerator - - - Adgangskodetip - - - Vi har sendt dig en e-mail med dit hovedadgangskodetip. - - - Er du sikker du vil overskrive den aktuelle adgangskode? - - - Bitwarden holder automatisk din boks synkroniseret ved hjælp af push-notifikationer. For den bedst mulige oplevelse bør du vælge "Tillad" i den følgende prompt for at aktivere push-notifikationer. - Push notifications for apple products - - - Bedøm appen - - - Overvej venligst at hjælpe os med en god anmeldelse! - - - App Store-bedømmelser nulstilles ved hver ny version af Bitwarden. Overvej venligst at hjælpe os med en god anmeldelse! - - - Regenerér adgangskode - - - Angiv hovedadgangskode igen - - - Søg i boks - - - Sikkerhed - - - Se udviklingsforløb - - - Vælg - - - Sæt PIN-kode - - - Angiv en 4-cifret PIN-kode til at låse appen op med. - - - Elementinformation - - - Element opdateret. - - - Indsender... - Message shown when interacting with the server - - - Synkroniserer... - Message shown when interacting with the server - - - Synkronisering fuldført. - - - Synkronisering mislykkedes. - - - Synkronisér boks nu - - - Touch ID - What Apple calls their fingerprint reader. - - - To-trins login - - - To-trins login øger din kontosikkerhed, ved at kræve at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificerings app, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu? - - - Lås op med {0} - - - Lås op med PIN-kode - - - Validerer - Message shown when interacting with the server - - - Verifikationskode - - - Vis element - - - Bitwarden web-boks - - - Håndtér dine elementer fra enhver webbrowser med Bitwarden web-boksen. - - - Mistet autentificerings-app? - - - Elementer - Screen title - - - Udvidelse aktiveret! - - - Ikoner - - - Oversættelser - - - Elementer til {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Der er ingen elementer i din boks til {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Når du ser en Bitwarden autoudfyldningsnotifikation, kan du trykke på den for at starte automatisk udfyldning. - - - Tryk på denne notifikation for at autoudfylde med et element fra din boks. - - - Åbn indstillinger for hjælpefunktioner - - - 1. Tryk fra Androids Hjælpefunktioner på "Bitwarden" under Tjenester-sektionen. - - - 2. Slå kontakten til og tryk på OK for at acceptere. - - - Deaktiveret - - - Aktiveret - - - Status - - - Beta - - - Den letteste måde at tilføje nye logins til din boks er fra Bitwarden Autoudfyldningstjenesten. Få mere at vide om brugen af Bitwarden Autoudfyldningstjenesten ved at gå til skærmen "Værktøjer". - - - Autoudfyld - - - Vil du autoudfylde eller se dette element? - - - Er du sikker på du vil autoudfylde dette element? Det er ikke et fuldstændigt match til "{0}". - - - Matchende elementer - - - Mulige matchende elementer - - - Søg - - - Du søger efter et autoudfyld element til "{0}". - - - Del din boks - - - Opret en organisation for at dele dine elementer med andre brugere på en sikker måde. - - - Skan når adgangskodefeltet fokuseres - - - Skan kun skærmen for felter og tilbyd en autoudfyld notifikation, når du vælger et adgangskodefelt. Denne indstilling kan hjælpe med at spare på batteriet. - - - Vedvarende notifikation - - - Tilbyd altid en autoudfyld notifikation og skan kun efter felter efter forsøgt autoudfyld. Denne indstilling kan hjælpe med at spare på batteriet. - - - Skan altid - - - Skan altid skærmen for felter og tilbyd kun en autoudfyld notifikation, hvis adgangskodefelter er fundet. Dette er standardindstillingen. - - - Kan ikke åbne appen "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Autentificerings app - For 2FA - - - Indtast den 6-cifrede verifikationskode fra din autentificeringsapp. - For 2FA - - - Indtast den 6-cifrede verifikationskode der blev sendt til {0}. - For 2FA - - - Login utilgængelig - For 2FA whenever there are no available providers on this device. - - - Denne konto har to-trins login aktiveret, men ingen af de konfigurerede to-trins-udbydere understøttes på denne enhed. Du skal bruge en understøttet enhed og/eller tilføje ekstra udbydere, der understøttes bedre på tværs af enheder (såsom en autentificeringsapp). - - - Gendannelseskode - For 2FA - - - Husk mig - Remember my two-step login - - - Send verifikationskode-email igen - For 2FA - - - To-trins-login indstillinger - - - Brug en anden to-trins-loginmetode - - - Kunne ikke sende bekræftelses-email. Prøv igen. - For 2FA - - - Bekræftelses-email sendt. - For 2FA - - - For at fortsætte, skal du holde din YubiKey NEO mod bagsiden af enheden eller indsætte din YubiKey i enhedens USB-port, og trykke på dens knap. - - - YubiKey sikkerhedsnøgle - "YubiKey" is the product name and should not be translated. - - - Tilføj ny vedhæftning - - - Vedhæftninger - - - Kunne ikke hente fil. - - - Din enhed kan ikke åbne denne type fil. - - - Henter... - Message shown when downloading a file - - - Størrelsen på denne vedhæftning er {0}. Er du sikker på at du hente den ned på din enhed? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Autentificeringsnøgle (TOTP) - - - Bekræftelseskode (TOTP) - Totp code label - - - Autentificeringsnøgle tilføjet. - - - Kan ikke læse autentificeringsnøgle. - - - Skanning vil ske automatisk. - - - Peg dit kamera mod QR-koden. - - - Skan QR-kode - - - Kamera - - - Billeder - - - TOTP kopieret! - - - Kopiér TOTP - - - Hvis dit login har en autentificeringsnøgle tilknyttet, kopieres TOTP verifikationskoden automatisk til din udklipsholder når du auto-udfylder login. - - - Deaktivér automatisk TOTP kopiering - - - Premium-medlemskab kræves for at anvende denne funktion. - - - Vedhæftning tilføjet - - - Vedhæftning slettet - - - Vælg fil - - - Fil - - - Ingen fil valgt - - - Der er ingen vedhæftninger. - - - Fil kilde - - - Funktion utilgængelig - - - Maksimum filstørrelse er 100 MB. - - - Du kan ikke bruge denne funktion, før du opdaterer din krypteringsnøgle. - - - Få mere at vide - - - API server URL - - - Brugerdefineret miljø - - - Til avancerede brugere. Du kan angive grund URL'en for hver tjeneste uafhængigt. - - - Miljøets URLs er blevet gemt. - - - {0} er ikke formateret korrekt. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identitetsserver URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Selv-hostet miljø - - - Angiv grund-URL'en til din lokal-hostede Bitwarden-installation. - - - Server URL - - - Web-boks server URL - - - Tryk på denne notifikation for at få vist elementer fra din boks. - - - Brugerdefinerede felter - - - Kopiér nummer - - - Kopiér sikkerhedskode - - - Nummer - - - Sikkerhedskode - - - Hvilken type element vil du tilføje? - - - Kort - - - Identitet - - - Login - - - Sikker note - - - Adresse 1 - - - Adresse 2 - - - Adresse 3 - - - April - - - August - - - Type - - - Kortindehaverens navn - - - By - - - Virksomhed - - - Land - - - December - - - Dr - - - Udløbsmåned - - - Udløbsår - - - Februar - - - Fornavn - - - Januar - - - Juli - - - Juni - - - Efternavn - - - Kørekortnummer - - - Marts - - - Maj - - - Mellemnavn - - - Hr - - - Fru - - - Frk - - - November - - - Oktober - - - Pasnummer - - - Telefon - - - September - - - CPR-nummer - - - Region - - - Titel - - - Postnummer - - - Adresse - - - Udløb - - - Slå webikoner fra - - - Webikoner vises som et genkendeligt billede ved siden af hvert loginelement i din boks. - - - Ikonserver URL - - - Autoudfyld med Bitwarden - - - Boksen er låst - - - Gå til min boks - - - Samlinger - - - Der er ingen elementer i denne samling. - - - Der er ingen elementer i denne mappe. - - - Autoudfyld hjælpefunktion - - - Bitwarden autoudfyld tjenesten bruger Androids AutoFyld framework til at hjælpe med at udfylde logins, kreditkort og identitetsoplysninger i andre apps på din enhed. - - - Brug Bitwarden autoudfyld tjenesten til at udfylde logins, kreditkort og identitetsoplysninger i andre apps. - - - Åbn autoudfyld indstillinger - - - Face ID - What Apple calls their facial recognition reader. - - - Brug Face ID til at bekræfte. - - - Brug Face ID til at låse op - - - Bekræft Face ID - - - Lås op med Windows Hello - - - Bekræft med Windows Hello - - - Windows Hello - - - Vi var ude af stand til automatisk at åbne Android indstillingsmenuen AutoFyld-tjenesten for dig. Du kan manuelt navigere til AutoFyld indstillingsmenuen fra Android Indstillinger > System > Sprog og indtastning > Avanceret > AutoFyld-tjenesten. - - - Brugerdefineret felt navn - - - Boolsk - - - Skjult - - - Tekst - - - Nyt brugerdefineret felt - - - Hvilken type brugerdefineret felt vil du tilføje? - - - Fjern - - - Ny URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Grund-domæne - - - Standard - - - Nøjagtig - - - Vært - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regulært udtryk - A programming term, also known as 'RegEx'. - - - Begynder med - - - URI matchmetode - - - Matchmetode - URI match detection for auto-fill. - - - Ja, og gem - - - Autoudfyld og gem - - - Organisation - An entity of multiple related people (ex. a team or business organization). - - - Hold din Yubikey nær toppen af enheden. - - - Prøv igen - - - For at fortsætte skal du holde din YubiKey NEO mod bagsiden af enheden. - - - Hjælpefunktionen kan være nyttigt at bruge, når apps ikke understøtter den almindelige autoudfyldningstjeneste. - - - Adgangskode opdateret - ex. Date this password was updated - - - Opdateret - ex. Date this item was updated - - - Autoudfyld aktiveret! - - - Du skal logge ind på den primære Bitwarden-app, før du kan bruge Autoudfyld. - - - Dine logins kan nu nemt tilgås direkte fra dit tastatur, når du logger ind på apps og hjemmesider. - - - Vi anbefaler, at du deaktiverer andre Autoudfyld-apps under Indstillinger, hvis du ikke har planer om at bruge dem. - - - Få adgang til din boks direkte fra dit tastatur for hurtigt at autoudfylde adgangskoder. - - - Følg disse instruktioner for at aktivere autoudfyldning af adgangskoder på din enhed: - - - 1. Åbn iOS-appen "Indstillinger" - - - 2. Tryk på "Adgangskoder & konti" - - - 3. Tryk på "Autoudfyld adgangskoder" - - - 4. Aktivér Autoudfyld - - - 5. Vælg "Bitwarden" - - - Adgangskode Autoudfyld - - - Den letteste måde at tilføje nye logins til din boks er ved at bruge Bitwarden adgangskode Autoudfyld udvidelsen. Få mere at vide om brugen af Bitwarden adgangskode Autoudfyld udvidelsen ved at navigere til skærmbilledet "Værktøjer". - - diff --git a/src/App/Resources/AppResources.de.Designer.cs b/src/App/Resources/AppResources.de.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.de.resx b/src/App/Resources/AppResources.de.resx deleted file mode 100644 index fbdfec4c7..000000000 --- a/src/App/Resources/AppResources.de.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Über - - - Hinzufügen - Add/create a new entity (verb). - - - Ordner hinzufügen - - - Neuer Eintrag - The title for the add item page. - - - Es ist ein Fehler aufgetreten. - Alert title when something goes wrong. - - - Zurück - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Abbrechen - Cancel an operation. - - - Kopieren - Copy some value to your clipboard. - - - Passwort kopieren - The button text that allows a user to copy the login's password to their clipboard. - - - Nutzernamen kopieren - The button text that allows a user to copy the login's username to their clipboard. - - - Danksagungen - Title for page that we use to give credit to resources that we use. - - - Löschen - Delete an entity (verb). - - - Lösche... - Message shown when interacting with the server - - - Wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden. - Confirmation alert message when deleteing something. - - - Bearbeiten - - - Ordner bearbeiten - - - E-Mail - Short label for an email address. - - - E-Mail Adresse - Full label for a email address. - - - Schreibe uns - - - Kontaktiere uns direkt, um Hilfe zu bekommen oder um Feedback abzugeben. - - - PIN-Code eingeben. - - - Favoriten - Title for your favorite items in the vault. - - - Einen Fehler melden - - - Einen Fehler bei GitHub eintragen. - - - Mit Fingerabdruck bestätigen. - - - Ordner - Label for a folder. - - - Neuer Ordner wurde erstellt. - - - Ordner wurde gelöscht. - - - Kein Ordner - Items that have no folder specified go in this special "catch-all" folder. - - - Ordner - - - Ordner wurde aktualisiert. - - - Webseite besuchen - The button text that allows user to launch the website to their web browser. - - - Hilfe und Feedback - - - Verstecken - Hide a secret value that is currently shown (password). - - - Bitte verbinde dich mit dem Internet um fortzufahren. - Description message for the alert when internet connection is required to continue. - - - Internetverbindung erforderlich - Title for the alert when internet connection is required to continue. - - - Ungültiges Masterpasswort. Bitte erneut versuchen. - - - Ungültiger PIN. Bitte erneut versuchen. - - - Öffnen - The button text that allows user to launch the website to their web browser. - - - Anmelden - The login button text (verb). - - - Zugangsdaten - Title for login page. (noun) - - - Abmelden - The log out button text (verb). - - - Bist du sicher, dass du dich abmelden möchtest? - - - Masterpasswort - Label for a master password. - - - Mehr - Text to define that there are more options things to see. - - - Mein Tresor - The title for the vault page. - - - Name - Label for an entity name. - - - Nein - - - Notizen - Label for notes. - - - Ok - Acknowledgement. - - - Passwort - Label for a password. - - - Speichern - Button text for a save operation (verb). - - - Wird gespeichert... - Message shown when interacting with the server - - - Einstellungen - The title for the settings page. - - - Anzeigen - Reveal a hidden value (password). - - - Eintrag wurde gelöscht. - Confirmation message after successfully deleting a login. - - - Absenden - - - Synchronisieren - The title for the sync page. - - - Vielen Dank - - - Werkzeuge - The title for the tools page. - - - URI - Label for a uri/url. - - - Fingerabdruck zum Entsperren nutzen - - - Nutzername - Label for a username. - - - Das Feld {0} ist ein Pflichtfeld. - Validation message for when a form field is left blank and is required to be entered. - - - {0} wurde kopiert. - Confirmation message after suceessfully copying a value to the clipboard. - - - Fingerabdruck überprüfen - - - Masterpasswort überprüfen - - - PIN überprüfen - - - Version - - - Ansicht - - - Besuche unsere Website - - - Besuche unsere Webseite um Hilfe zu erhalten, Neuigkeiten zu erfahren, Kontakt aufzunehmen und mehr über die Verwendung von Bitwarden zu lernen. - - - Webseite - Label for a website. - - - Ja - - - Benutzerkonto - - - Dein neues Konto wurde erstellt! Du kannst dich jetzt anmelden. - - - Neuer Eintrag - - - App Erweiterung - - - Verwende den Bitwarden Dienst in den Bedienungshilfen, um deine Zugangsdaten in Apps und im Web automatisch ausfüllen zu lassen. - - - Auto-Ausfüllen Funktion - - - Mehrdeutige Zeichen vermeiden - - - Bitwarden App Erweiterung - - - Der einfachste Weg neue Zugangsdaten zum Tresor hinzuzufügen ist die Bitwarden App Erweiterung. Um mehr über die Bitwarden App Erweiterung zu erfahren, öffne einfach die "Werkzeuge" Bildschirmseite. - - - Nutze Bitwarden in Safari und anderen Apps, um Zugangsdaten automatisch einzufügen. - - - Bitwarden Auto-Ausfüllen Funktion - - - Verwende den Bitwarden Dienst in den Bedienungshilfen um deine Zugangsdaten automatisch einzufügen. - - - E-Mail Adresse ändern - - - Du kannst deine E-Mail Adresse im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen? - - - Masterpasswort ändern - - - Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen? - - - Schließen - - - Bald verfügbar! - - - Fortsetzen - - - Kopiert! - - - Passwort kopiert! - - - Nutzername kopiert! - - - Konto erstellen - - - Konto wird erstellt ... - Message shown when interacting with the server - - - Eintrag bearbeiten - - - Automatische Synchronisierung aktivieren - - - Gebe die E-Mail Adresse deines Kontos ein, um den Hinweis für dein Master-Passwort zu erhalten. - - - App Erweiterung wieder aktivieren - - - Fast geschafft! - - - App Erweiterung aktivieren - - - In Safari findest du Bitwarden unter dem Teilen-Symbol (Hinweis: scrolle auf der untersten Zeile des Menüs nach rechts). - Safari is the name of apple's web browser - - - Erhalte direkten Zugang zu deinen Passwörtern! - - - Du kannst dich jetzt anmelden! - - - Unterstützte Apps ansehen - - - Deine Zugangsdaten sind jetzt in Safari, Chrome und anderen unterstützten Apps einfach zu erreichen. - - - In Safari und Chrome findest du Bitwarden unter dem Teilen-Symbol (Hinweis: scrolle auf der untersten Zeile des Menüs nach rechts). - - - Tippe auf das Bitwarden Symbol im Menü, um die Erweiterung zu starten. - - - Um Bitwarden in Safari und anderen Apps zu aktivieren, tippe auf das "Mehr"-Symbol in der untersten Zeile des Menüs. - - - Favorit - - - Fingerabdruck - - - Passwort erstellen - - - Hinweis zum Masterpasswort zusenden - - - Einträge importieren - - - Du kannst deine Logins über den Bitwarden.com Web-Tresor importieren. Möchtest du die Seite jetzt öffnen? - - - Importiere all deine Zugangsdaten schnell und einfach von anderen Passwortmanagern. - - - Zuletzt synchronisiert: - - - Länge - - - Sperren - - - 15 Minuten - - - 1 Stunde - - - 1 Minute - - - 4 Stunden - - - Sofort - - - Sperroptionen - - - Anmeldung läuft... - Message shown when interacting with the server - - - Du musst dich anmelden oder einen neuen Account erstellen um auf den Tresor zuzugreifen. - - - Verwalten - - - Passwortbestätigung ist nicht korrekt. - - - Das Master-Passwort wird verwendet, um den Tresor zu öffnen. Es ist sehr wichtig, dass du das Passwort nicht vergisst, da es keine Möglichkeit gibt es zurückzusetzen. - - - Masterpassworthinweis (optional) - - - Ein Hinweis auf dein Master-Passwort kann dir helfen, dich an das Passwort zu erinnern, solltest du es vergessen. - - - Das Masterpasswort muss mindestens 8 Zeichen lang sein. - - - Mindestanzahl Zahlen - Minimum numeric characters for password generator settings - - - Mindestanzahl Sonderzeichen - Minimum special characters for password generator settings - - - Weitere Einstellungen - - - Du musst dich in der Bitwarden App einloggen, bevor du die Erweiterung benutzen kannst. - - - Niemals - - - Eintrag erstellt. - - - Es befinden sich keine Favoriten in Ihrem Tresor. - - - Keine Einträge in deinem Tresor. - - - Es befinden sich keine Zugangsdaten für diese Webseite in deinem Tresor. Zum Hinzufügen tippen. - - - Diese Zugangsdaten sind noch nicht konfiguriert. - - - Ok, verstanden! - Confirmation, like "Ok, I understand it" - - - Die Standardeinstellungen können mit dem Passwortgenerator der Bitwarden App definiert werden. - - - Optionen - - - Sonstige - - - Passwort generiert. - - - Passwortgenerator - - - Passworthinweis - - - Wir haben Ihnen eine E-Mail mit Ihrem Masterpassworthinweis gesendet. - - - Bist du sicher, dass du das aktuelle Passwort überschreiben möchtest? - - - Bitwarden aktualisiert deinen Tresor mit Pushbenachrichtigungen. Für die bestmögliche Benutzererfahrung tippe im folgenden Dialogfenster auf "Ok", um Pushbenachrichtigungen zu aktivieren. - Push notifications for apple products - - - App bewerten - - - Wir würden uns freuen, wenn du uns mit einer positiven Bewertung helfen könntest! - - - Bewertungen für die App werden mit jeder neuen Version zurückgesetzt. Unterstütze uns mit einer guten Rezension! - - - Passwort neu generieren - - - Masterpasswort wiederholen - - - Tresor durchsuchen - - - Sicherheit - - - Stand der Entwicklung ansehen - - - Auswählen - - - PIN festlegen - - - Gebe deine 4-stellige PIN ein, um die App zu entsperren. - - - Eintrags-Information - - - Eintrag aktualisiert. - - - Wird übermittelt... - Message shown when interacting with the server - - - Wird synchronisiert... - Message shown when interacting with the server - - - Synchronisierung abgeschlossen. - - - Synchronisierung fehlgeschlagen. - - - Tresor jetzt synchronisieren - - - Touch ID - What Apple calls their fingerprint reader. - - - Zwei-Faktor Authentifizierung - - - Mit der Zwei-Faktor Authentifizierung wird dein Account zusätzlich abgesichert, da jede Anmeldung durch einen Sicherheitscode, eine Authentifizierungs-App, SMS, einen Anruf oder eine E-Mail verifiziert werden muss. Die Zwei-Faktor Authentifizierung kann im Bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Seite jetzt öffnen? - - - Mit {0} entsperren - - - Mit PIN-Code entsperren - - - Validierung - Message shown when interacting with the server - - - Bestätigungscode - - - Eintrag anzeigen - - - Bitwarden Web-Tresor - - - Verwalte deine Zugangsdaten in jedem Browser mit dem bitwarden Web-Tresor. - - - Authentifizierungs-App verloren? - - - Einträge - Screen title - - - Erweiterung aktiviert! - - - Icons - - - Übersetzungen - - - Zugangsdaten für {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Es befinden sich keine Zugangsdaten für {0} in deinem Tresor. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Du kannst die Bitwarden Auto-Ausfüllen Benachrichtigung antippen, um deine Zugangsdaten automatisch einfügen zu lassen. - - - Tippe auf diese Benachrichtigung, um automatisch Zugangsdaten aus deinem Tresor einzufügen. - - - Einstellungen für Bedienungshilfen öffnen - - - 1. Tippe auf "Bitwarden" unter "Heruntergeladene Dienste" in den Android Bedienungshilfen. - - - 2. Aktiviere den Schalter und tippe auf "Ok". - - - Deaktiviert - - - Aktiviert - - - Status - - - Beta - - - Der einfachste Weg, neue Zugangsdaten zum Tresor hinzuzufügen, ist die Bitwarden Auto-Ausfüllen Funktion. Um mehr über die Bitwarden Auto-Ausfüllen Funktion zu erfahren, öffne einfach "Werkzeuge" in der App Erweiterung. - - - Auto-Ausfüllen - - - Möchtest du diese Zugangsdaten automatisch einfügen oder ansehen? - - - Bist du sicher, dass du die Zugangsdaten einfügen möchten? Sie stimmen nicht komplett mit "{0}" überein. - - - Passende Einträge - - - Möglicherweise passende Einträge - - - Durchsuchen - - - Du suchst nach einem Auto-Fill Objekt für "{0}". - - - Ihren Tresor teilen - - - Erstelle eine Organisation um deine Logindaten sicher für andere Benutzer freizugeben. - - - Scan bei ausgewähltem Passwortfeld durchführen - - - Den Bildschirminhalt nur auf Loginfelder untersuchen und Auto-Ausfüllen Benachrichtigung zeigen, wenn ein Passwortfeld ausgewählt ist. Diese Option kann die Akkulaufzeit verlängern. - - - Dauerhafte Benachrichtigung - - - Die Auto-Ausfüllen Benachrichtigung immer zeigen, aber den Bildschirminhalt erst durchsuchen wenn die Benachrichtigung ausgewählt wurde. Diese Option kann die Akkulaufzeit verlängern. - - - Bildschirminhalt immer durchsuchen - - - Den Bildschirminhalt immer auf Loginfelder untersuchen und eine Auto-Ausfüllen Benachrichtigung anbieten, wenn ein Loginfeld gefunden wurde. Das ist die Standardeinstellung. - - - Die App "{0}" kann nicht geöffnet werden. - Message shown when trying to launch an app that does not exist on the user's device. - - - Authentifizierungs-App - For 2FA - - - Gib den 6-stelligen Verifizierungscode aus deiner Authenticator App ein. - For 2FA - - - Gib den 6-stelligen Bestätigungscode ein, der an {0} gesendet wurde. - For 2FA - - - Zugangsdaten nicht verfügbar - For 2FA whenever there are no available providers on this device. - - - Dieses Konto hat eine aktive Zwei-Faktor Authentifizierung, allerdings wird keiner der konfigurierten Zwei-Faktor Anbieter von diesem Gerät unterstützt. Bitte nutze ein unterstütztes Gerät und / oder füge zusätzliche Anbieter hinzu, die von mehr Geräten unterstützt werden (wie eine Authentifizierungs-App). - - - Wiederherstellungscode - For 2FA - - - Eingeloggt bleiben - Remember my two-step login - - - E-Mail mit Bestätigungscode erneut versenden - For 2FA - - - Optionen für Zwei-Faktor Authentifizierung - - - Verwende eine andere Zwei-Faktor Authentifizierungsmethode - - - Bestätigungsmail konnte nicht gesendet werden. Erneut versuchen. - For 2FA - - - Bestätigungsmail wurde gesendet. - For 2FA - - - Halte deinen YubiKey NEO an die Rückseite des Geräts, um fortzufahren. - - - YubiKey NEO Sicherheitsschlüssel - "YubiKey" is the product name and should not be translated. - - - Anhang hinzufügen - - - Anhänge - - - Datei kann nicht heruntergeladen werden. - - - Ihr Gerät kann diesen Dateitypen nicht öffnen. - - - Wird heruntergeladen... - Message shown when downloading a file - - - Dieser Anhang ist {0} groß. Bist du sicher, dass du ihn herunterladen möchtest? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Authentifizierungsschlüssel (TOTP) - - - Bestätigungscode (TOTP) - Totp code label - - - Authentifizierungsschlüssel hinzugefügt. - - - Authentifizierungsschlüssel kann nicht gelesen werden. - - - Der Scan wird automatisch durchgeführt. - - - Richte deine Kamera auf den QR Code. - - - QR Code scannen - - - Kamera - - - Fotos - - - TOTP kopiert! - - - TOTP kopieren - - - Ist ein Authentifizierungsschlüssel mit deinen Zugangsdaten verknüpft, wird der TOTP Bestätigungscode automatisch in die Zwischenablage kopiert, wenn du die Zugangsdaten einfügen lässt. - - - Automatisches Kopieren des TOTP deaktivieren - - - Für diese Funktion benötigst du eine Premiummitgliedschaft. - - - Anhang hinzugefügt - - - Anhang entfernt - - - Datei auswählen - - - Datei - - - Keine Datei ausgewählt - - - Keine Anhänge verfügbar. - - - Dateiquelle - - - Funktion nicht verfügbar - - - Die maximale Dateigröße beträgt 100 MB. - - - Du kannst diese Funktion nicht nutzen, solange du deinen Verschlüsselungsschlüssel nicht aktualisiert hast. - - - Mehr erfahren - - - API Server URL - - - Benutzerdefinierte Umgebung - - - Für fortgeschrittene Benutzer. Du kannst die Basis-URL der jeweiligen Dienste unabhängig voneinander festlegen. - - - Die URLs der Umgebung wurden gespeichert. - - - {0} ist nicht korrekt formatiert. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identitätsserver-URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Selbstgehostete Umgebung - - - Bitte gebe die Basis-URL deiner selbst gehosteten Bitwarden-Installation an. - - - Server URL - - - Web-Tresor Server URL - - - Tippe auf diese Benachrichtigung um Einträge aus deinem Tresor anzusehen. - - - Benutzerdefinierte Felder - - - Nummer kopieren - - - Sicherheitscode kopieren - - - Nummer - - - Sicherheitscode - - - Welche Art Eintrag möchtest du hinzufügen? - - - Karte - - - Identität - - - Zugangsdaten - - - Sichere Notiz - - - Adresse 1 - - - Adresse 2 - - - Adresse 3 - - - April - - - August - - - Marke - - - Name des Karteninhabers - - - Stadt oder Ort - - - Firma - - - Land - - - Dezember - - - Dr - - - Ablaufmonat - - - Ablaufjahr - - - Februar - - - Vorname - - - Januar - - - Juli - - - Juni - - - Nachname - - - Führerscheinnummer - - - März - - - Mai - - - Zweiter Vorname - - - Herr - - - Frau - - - Frau - - - November - - - Oktober - - - Reisepassnummer - - - Telefon - - - September - - - Sozialversicherungsnummer - - - Bundesland - - - Titel - - - Postleitzahl - - - Adresse - - - Gültig bis - - - Icons der Website deaktivieren - - - Website-Symbole zeigen ein erkennbares Bild neben jedem Login in deinem Tresor. - - - Icons Server URL - - - Mit Bitwarden automatisch ausfüllen - - - Tresor ist gesperrt - - - Zum Tresor gehen - - - Sammlungen - - - Es sind keine Objekte in dieser Sammlung. - - - Es sind keine Objekte in diesem Ordner. - - - Auto-Fill Bedienungshilfe - - - Der Bitwarden Auto-Fill Service benutzt das Android Autofill Framework, um Logins, Kreditkarten und Identitätsinformationen in anderen Apps auf Ihrem Gerät zu befüllen. - - - Verwende den Bitwarden Dienst in den Bedienungshilfen, um deine Zugangsdaten automatisch einzufügen. - - - Öffne Auto-Fill Einstellungen - - - Face ID - What Apple calls their facial recognition reader. - - - Verwende Face ID zum Verifizieren. - - - Face ID zum Entsperren verwenden - - - Face ID überprüfen - - - Mit Windows Hello entsperren - - - Mit Windows Hello überprüfen - - - Windows Hello - - - Die Android Auto-Fill Einstellungen konnten nicht automatisch geöffnet werden. Über Android Einstellungen > Sprachen & Eingabe > AutoFill-Dienst kannst du manuell zu den Auto-Fill Einstellungen navigieren. - - - Name Benutzerdefiniertes Feld - - - Boolean - - - Versteckt - - - Text - - - Neues benutzerdefiniertes Feld - - - Welche Art Eintrag möchtest du hinzufügen? - - - Entfernen - - - Neue URL - - - URL {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Basis-Domäne - - - Standard - - - Exakt - - - Server - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regulärer Ausdruck - A programming term, also known as 'RegEx'. - - - Beginnt mit - - - Match-Erkennung - - - Match-Erkennung - URI match detection for auto-fill. - - - Ja, und speichern - - - Automatisch ausfüllen und speichern - - - Organisation - An entity of multiple related people (ex. a team or business organization). - - - Halte deinen Yubikey an den oberen Bereich deines Geräts. - - - Erneut versuchen - - - Um fortzufahren, halte deinen YubiKey NEO gegen die Rückseite des Gerätes. - - - Der Barrierefreiheits-Dienst kann möglicherweise hilfreich sein, wenn apps nicht den standard AutoFill-Dienst unterstützen. - - - Passwort aktualisiert - ex. Date this password was updated - - - Aktualisiert - ex. Date this item was updated - - - AutoFill aktiviert! - - - Du musst dich in der Bitwarden App einloggen, bevor du AutoFill nutzen kannst. - - - Du kannst nun direkt von der Tastatur auf deine Zugangsdaten zugreifen, wenn du dich auf Webseiten oder in Apps anmeldest. - - - Wir empfehlen alle anderen Auto Ausfüllen Apps in den Einstellungen zu deaktivieren wenn du sie nicht nutzt. - - - Greife auf deinen Tressor direkt von deiner Tastatur aus zu um Passwörter schnell und automatisch auszufüllen. - - - Um Autofill auf deinem Gerät zu aktivieren, befolge bitte diese Anweisungen: - - - 1. Gehe in die iOS Einstellungen - - - 2. Drücke "Passwörter & Accounts" - - - 3. Drücke "AutoFill Passwörter" - - - 4. Schalte AutoFill ein - - - 5. Wähle "Bitwarden" - - - Passwort AutoFill - - - Der einfachste Weg neue Zugangsdaten zum Tresor hinzuzufügen ist die Bitwarden Passwort Autofill Erweiterung. Um mehr über die Bitwarden Passwort Autofill Erweiterung zu erfahren, öffne einfach die "Werkzeuge" Bildschirmseite. - - diff --git a/src/App/Resources/AppResources.en-GB.resx b/src/App/Resources/AppResources.en-GB.resx deleted file mode 100644 index 23cd5f2b0..000000000 --- a/src/App/Resources/AppResources.en-GB.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - About - - - Add - Add/create a new entity (verb). - - - Add folder - - - Add item - The title for the add item page. - - - An error has occurred. - Alert title when something goes wrong. - - - Back - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Cancel - Cancel an operation. - - - Copy - Copy some value to your clipboard. - - - Copy password - The button text that allows a user to copy the login's password to their clipboard. - - - Copy username - The button text that allows a user to copy the login's username to their clipboard. - - - Credits - Title for page that we use to give credit to resources that we use. - - - Delete - Delete an entity (verb). - - - Deleting... - Message shown when interacting with the server - - - Do you really want to delete? This cannot be undone. - Confirmation alert message when deleteing something. - - - Edit - - - Edit folder - - - Email - Short label for an email address. - - - Email address - Full label for a email address. - - - Email us - - - Email us directly to get help or leave feedback. - - - Enter your PIN code. - - - Favourites - Title for your favorite items in the vault. - - - File a bug report - - - Open an issue at our GitHub repository. - - - Use your fingerprint to verify. - - - Folder - Label for a folder. - - - New folder created. - - - Folder deleted. - - - No folder - Items that have no folder specified go in this special "catch-all" folder. - - - Folders - - - Folder updated. - - - Go to website - The button text that allows user to launch the website to their web browser. - - - Help and feedback - - - Hide - Hide a secret value that is currently shown (password). - - - Please connect to the Internet before continuing. - Description message for the alert when internet connection is required to continue. - - - Internet connection required - Title for the alert when internet connection is required to continue. - - - Invalid master password. Try again. - - - Invalid PIN. Try again. - - - Launch - The button text that allows user to launch the website to their web browser. - - - Log in - The login button text (verb). - - - Login - Title for login page. (noun) - - - Log out - The log out button text (verb). - - - Are you sure you want to log out? - - - Master password - Label for a master password. - - - More - Text to define that there are more options things to see. - - - My vault - The title for the vault page. - - - Name - Label for an entity name. - - - No - - - Notes - Label for notes. - - - OK - Acknowledgement. - - - Password - Label for a password. - - - Save - Button text for a save operation (verb). - - - Saving... - Message shown when interacting with the server - - - Settings - The title for the settings page. - - - Show - Reveal a hidden value (password). - - - Item has been deleted. - Confirmation message after successfully deleting a login. - - - Submit - - - Sync - The title for the sync page. - - - Thank you - - - Tools - The title for the tools page. - - - URI - Label for a uri/url. - - - Use fingerprint to unlock - - - Username - Label for a username. - - - The {0} field is required. - Validation message for when a form field is left blank and is required to be entered. - - - {0} has been copied. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verify fingerprint - - - Verify master password - - - Verify PIN - - - Version - - - View - - - Visit our website - - - Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden. - - - Website - Label for a website. - - - Yes - - - Account - - - Your new account has been created! You may now log in. - - - Add an item - - - App extension - - - Use the Bitwarden accessibility service to auto-fill your logins across apps and the web. - - - Auto-fill service - - - Avoid ambiguous characters - - - Bitwarden app extension - - - The easiest way to add new logins to your vault is from the Bitwarden app extension. Learn more about using the Bitwarden app extension by navigating to the "Tools" screen. - - - Use Bitwarden in Safari and other apps to auto-fill your logins. - - - Bitwarden auto-fill service - - - Use the Bitwarden accessibility service to auto-fill your logins. - - - Change email - - - You can change your email address on the bitwarden.com web vault. Do you want to visit the website now? - - - Change master password - - - You can change your master password on the bitwarden.com web vault. Do you want to visit the website now? - - - Close - - - Coming soon! - - - Continue - - - Copied! - - - Copied password! - - - Copied username! - - - Create account - - - Creating account... - Message shown when interacting with the server - - - Edit item - - - Enable automatic syncing - - - Enter your account email address to receive your master password hint. - - - Re-enable app extension - - - Almost done! - - - Enable app extension - - - In Safari, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu). - Safari is the name of apple's web browser - - - Get instant access to your passwords! - - - You're ready to log in! - - - See supported apps - - - Your logins are now easily accessible from Safari, Chrome, and other supported apps. - - - In Safari and Chrome, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the share menu). - - - Tap the Bitwarden icon in the menu to launch the extension. - - - To turn on Bitwarden in Safari and other apps, tap the "more" icon on the bottom row of the menu. - - - Favourite - - - Fingerprint - - - Generate password - - - Get your master password hint - - - Import items - - - You can bulk import items from the bitwarden.com web vault. Do you want to visit the website now? - - - Quickly bulk import your items from other password management apps. - - - Last sync: - - - Length - - - Lock - - - 15 minutes - - - 1 hour - - - 1 minute - - - 4 hours - - - Immediately - - - Lock options - - - Logging in... - Message shown when interacting with the server - - - Log in or create a new account to access your secure vault. - - - Manage - - - Password confirmation is not correct. - - - The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it. - - - Master password hint (optional) - - - A master password hint can help you remember your password if you forget it. - - - Master password must be at least 8 characters long. - - - Minimum numbers - Minimum numeric characters for password generator settings - - - Minimum special - Minimum special characters for password generator settings - - - More settings - - - You must log into the main Bitwarden app before you can use the extension. - - - Never - - - New item created. - - - There are no favourites in your vault. - - - There are no items in your vault. - - - There are no items in your vault for this website/app. Tap to add one. - - - This login does not have a username or password configured. - - - OK, got it! - Confirmation, like "Ok, I understand it" - - - Option defaults are set from the main Bitwarden app's password generator tool. - - - Options - - - Other - - - Password generated. - - - Password generator - - - Password hint - - - We've sent you an email with your master password hint. - - - Are you sure you want to overwrite the current password? - - - Bitwarden keeps your vault automatically synced by using push notifications. For the best possible experience, please select "Allow" on the following prompt when asked to enable push notifications. - Push notifications for apple products - - - Rate the app - - - Please consider helping us out with a good review! - - - App Store ratings are reset with every new version of Bitwarden. Please consider helping us out with a good review! - - - Regenerate password - - - Re-type master password - - - Search vault - - - Security - - - See development progress - - - Select - - - Set PIN - - - Enter a 4 digit PIN code to unlock the app with. - - - Item information - - - Item updated. - - - Submitting... - Message shown when interacting with the server - - - Syncing... - Message shown when interacting with the server - - - Syncing complete. - - - Syncing failed. - - - Sync vault now - - - Touch ID - What Apple calls their fingerprint reader. - - - Two-step login - - - Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now? - - - Unlock with {0} - - - Unlock with PIN code - - - Validating - Message shown when interacting with the server - - - Verification code - - - View item - - - Bitwarden web vault - - - Manage your items from any web browser with the Bitwarden web vault. - - - Lost authenticator app? - - - Items - Screen title - - - Extension activated! - - - Icons - - - Translations - - - Items for {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - There are no items in your vault for {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - When you see a Bitwarden auto-fill notification, you can tap it to launch the auto-fill service. - - - Tap this notification to auto-fill an item from your vault. - - - Open accessibility settings - - - 1. On the Android accessibility settings screen, touch "Bitwarden" under the Services heading. - - - 2. Switch on the toggle and press OK to accept. - - - Disabled - - - Enabled - - - Status - - - Beta - - - The easiest way to add new logins to your vault is from the Bitwarden auto-fill service. Learn more about using the Bitwarden auto-fill service by navigating to the "Tools" screen. - - - Auto-fill - - - Do you want to auto-fill or view this item? - - - Are you sure you want to auto-fill this item? It is not a complete match for "{0}". - - - Matching items - - - Possible matching items - - - Search - - - You are searching for an auto-fill item for "{0}". - - - Share your vault - - - Create an organisation to share your items securely with other users. - - - Scan when password field focused - - - Only scan the screen for fields and offer an auto-fill notification whenever you select a password field. This setting may help conserve battery life. - - - Persist notification - - - Always offer an auto-fill notification and only scan for fields after attempting an auto-fill. This setting may help conserve battery life. - - - Always scan - - - Always scan the screen for fields and only offer an auto-fill notification if password fields are found. This is the default setting. - - - Cannot open the app "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Authenticator app - For 2FA - - - Enter the 6 digit verification code from your authenticator app. - For 2FA - - - Enter the 6 digit verification code that was emailed to {0}. - For 2FA - - - Login unavailable - For 2FA whenever there are no available providers on this device. - - - This account has two-step login enabled. However, none of the configured two-step providers are supported on this device. Please use a supported device and/or add additional providers that are better supported across devices (such as an authenticator app). - - - Recovery code - For 2FA - - - Remember me - Remember my two-step login - - - Send verification code email again - For 2FA - - - Two-step login options - - - Use another two-step login method - - - Could not send verification email. Try again. - For 2FA - - - Verification email sent. - For 2FA - - - To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device's USB port, then touch its button. - - - YubiKey security key - "YubiKey" is the product name and should not be translated. - - - Add new attachment - - - Attachments - - - Unable to download file. - - - Your device cannot open this type of file. - - - Downloading... - Message shown when downloading a file - - - This attachment is {0} in size. Are you sure you want to download it onto your device? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Authenticator key (TOTP) - - - Verification code (TOTP) - Totp code label - - - Authenticator key added. - - - Cannot read authenticator key. - - - Scanning will happen automatically. - - - Point your camera at the QR code. - - - Scan QR code - - - Camera - - - Photos - - - Copied TOTP! - - - Copy TOTP - - - If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login. - - - Disable automatic TOTP Copy - - - A premium membership is required to use this feature. - - - Attachment added - - - Attachment deleted - - - Choose file - - - File - - - No file chosen - - - There are no attachments. - - - File source - - - Feature unavailable - - - Maximum file size is 100 MB. - - - You cannot use this feature until you update your encryption key. - - - Learn more - - - API server URL - - - Custom environment - - - For advanced users. You can specify the base URL of each service independently. - - - The environment URLs have been saved. - - - {0} is not correctly formatted. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identity server URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Self-hosted environment - - - Specify the base URL of your on-premise hosted Bitwarden installation. - - - Server URL - - - Web vault server URL - - - Tap this notification to view items from your vault. - - - Custom fields - - - Copy number - - - Copy security code - - - Number - - - Security code - - - What type of item do you want to add? - - - Card - - - Identity - - - Login - - - Secure note - - - Address 1 - - - Address 2 - - - Address 3 - - - April - - - August - - - Brand - - - Cardholder name - - - City / town - - - Company - - - Country - - - December - - - Dr - - - Expiration month - - - Expiration year - - - February - - - First name - - - January - - - July - - - June - - - Last name - - - Licence number - - - March - - - May - - - Middle name - - - Mr - - - Mrs - - - Ms - - - November - - - October - - - Passport number - - - Phone - - - September - - - National Insurance number - - - County - - - Title - - - Postcode - - - Address - - - Expiration - - - Disable website icons - - - Website icons provide a recognisable image next to each login item in your vault. - - - Icons server URL - - - Auto-fill with Bitwarden - - - Vault is locked - - - Go to my vault - - - Collections - - - There are no items in this collection. - - - There are no items in this folder. - - - Auto-fill accessibility service - - - The Bitwarden auto-fill service uses the Android autofill framework to assist in filling logins, credit cards, and identity information into other apps on your device. - - - Use the Bitwarden auto-fill service to fill logins, credit cards, and identity information into other apps. - - - Open auto-fill settings - - - Face ID - What Apple calls their facial recognition reader. - - - Use Face ID to verify. - - - Use Face ID to unlock - - - Verify Face ID - - - Unlock with Windows Hello - - - Verify with Windows Hello - - - Windows Hello - - - We were unable to automatically open the Android auto-fill settings menu for you. You can navigate to the auto-fill settings menu manually from Android Settings > System > Languages and input > Advanced > Autofill service. - - - Custom field name - - - Boolean - - - Hidden - - - Text - - - New custom field - - - What type of custom field do you want to add? - - - Remove - - - New URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Base domain - - - Default - - - Exact - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regular expression - A programming term, also known as 'RegEx'. - - - Starts with - - - URI match detection - - - Match detection - URI match detection for auto-fill. - - - Yes, and save - - - Auto-fill and save - - - Organisation - An entity of multiple related people (ex. a team or business organization). - - - Hold your Yubikey near the top of the device. - - - Try again - - - To continue, hold your YubiKey NEO against the back of the device. - - - The accessibility service may be helpful to use when apps do not support the standard auto-fill service. - - - Password updated - ex. Date this password was updated - - - Updated - ex. Date this item was updated - - - Auto-fill activated! - - - You must log into the main Bitwarden app before you can use auto-fill. - - - Your logins are now easily accessible right from your keyboard while logging into apps and websites. - - - We recommend disabling any other auto-fill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly auto-fill passwords. - - - To enable password auto-fill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden password auto-fill extension. Learn more about using the Bitwarden password auto-fill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.es.Designer.cs b/src/App/Resources/AppResources.es.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.es.resx b/src/App/Resources/AppResources.es.resx deleted file mode 100644 index 0308a8927..000000000 --- a/src/App/Resources/AppResources.es.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Acerca de - - - Añadir - Add/create a new entity (verb). - - - Añadir carpeta - - - Añadir elemento - The title for the add item page. - - - Ha ocurrido un error. - Alert title when something goes wrong. - - - Atrás - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Cancelar - Cancel an operation. - - - Copiar - Copy some value to your clipboard. - - - Copiar contraseña - The button text that allows a user to copy the login's password to their clipboard. - - - Copiar usuario - The button text that allows a user to copy the login's username to their clipboard. - - - Créditos - Title for page that we use to give credit to resources that we use. - - - Eliminar - Delete an entity (verb). - - - Eliminando... - Message shown when interacting with the server - - - ¿Seguro que quieres eliminarlo? Esto no puedes deshacerse. - Confirmation alert message when deleteing something. - - - Editar - - - Editar carpeta - - - Correo electrónico - Short label for an email address. - - - Correo electrónico - Full label for a email address. - - - Envíanos un correo - - - Envíanos un correo directamente para obtener ayuda o dejar tus comentarios. - - - Introduce to código PIN. - - - Favoritos - Title for your favorite items in the vault. - - - Reportar un fallo - - - Abrir una incidencia en el repositorio de GitHub. - - - Utilizar tu huella dactilar para continuar. - - - Carpeta - Label for a folder. - - - Nueva carpeta creada. - - - Carpeta eliminada. - - - Sin carpeta - Items that have no folder specified go in this special "catch-all" folder. - - - Carpetas - - - Carpeta actualizada. - - - Ir a la web - The button text that allows user to launch the website to their web browser. - - - Ayuda & Comentarios - - - Ocultar - Hide a secret value that is currently shown (password). - - - Por favor, conéctate a Internet antes de continuar. - Description message for the alert when internet connection is required to continue. - - - Conexión a Internet requerida - Title for the alert when internet connection is required to continue. - - - Contraseña maestra no válida. Prueba de nuevo. - - - PIN no válido. Prueba de nuevo. - - - Iniciar - The button text that allows user to launch the website to their web browser. - - - Identificarse - The login button text (verb). - - - Identificarse - Title for login page. (noun) - - - Cerrar sesión - The log out button text (verb). - - - ¿Estás seguro de querer cerrar sesión? - - - Contraseña maestra - Label for a master password. - - - Más - Text to define that there are more options things to see. - - - Mi caja fuerte - The title for the vault page. - - - Nombre - Label for an entity name. - - - No - - - Notas - Label for notes. - - - Aceptar - Acknowledgement. - - - Contraseña - Label for a password. - - - Guardar - Button text for a save operation (verb). - - - Guardando... - Message shown when interacting with the server - - - Ajustes - The title for the settings page. - - - Mostrar - Reveal a hidden value (password). - - - El elemento ha sido eliminado. - Confirmation message after successfully deleting a login. - - - Enviar - - - Sincronizar - The title for the sync page. - - - Gracias - - - Herramientas - The title for the tools page. - - - URI - Label for a uri/url. - - - Usar huella dactilar para desbloquear - - - Usuario - Label for a username. - - - El campo {0} es obligatorio. - Validation message for when a form field is left blank and is required to be entered. - - - {0} ha sido copiado. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verificar huella dactilar - - - Verificar contraseña maestra - - - Verificar PIN - - - Versión - - - Ver - - - Visita nuestro sitio web - - - Visita nuestro sitio web para recibir ayuda, noticias, contactarnos por correo y/o aprender más sobre como utilizar Bitwarden. - - - Web - Label for a website. - - - Si - - - Cuenta - - - ¡Tu nueva cuenta ha sido creada! Ahora puedes acceder. - - - Añadir un elemento - - - Extensión de Aplicación - - - Utiliza el servicio de accesibilidad de Bitwarden para autorellenar entradas entre aplicaciones y sitios web. - - - Servicio de autocompletado - - - Evitar caracteres ambiguos - - - Extensión de Aplicación de Bitwarden - - - La forma mas fácil de añadir nuevas entradas a tu caja fuerte es con la Extensión de Aplicación de Bitwarden. Aprende más sobre como utilizar este servicio yendo a la sección "Herramientas". - - - Utiliza Bitwarden en Safari y otras aplicaciones para autorellenar tus entradas. - - - Servicio de Autorellenado de Bitwarden - - - Utiliza el servicio de accesibilidad de Bitwarden para autorellenar entradas. - - - Cambiar correo electrónico - - - Puedes cambiar tu correo electrónico en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web? - - - Cambiar contraseña maestra - - - Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web? - - - Cerrar - - - ¡Próximamente! - - - Continuar - - - ¡Copiado! - - - ¡Contraseña copiada! - - - ¡Nombre de usuario copiado! - - - Crear cuenta - - - Creando cuenta... - Message shown when interacting with the server - - - Editar elemento - - - Habilitar sincronizacion automatica - - - Introduce el correo electrónico de tu cuenta para recibir la pista de tu contraseña maestra. - - - Re-activar Extension de Aplicación - - - ¡Casi estamos! - - - Activar Extensión de Aplicación - - - En Safari, busca Bitwarden en el icono compartir (pista: desplazate a la derecha en la última fila del menú). - Safari is the name of apple's web browser - - - ¡Obtén acceso instantáneo a tus contraseñas! - - - ¡Ya estás identificado! - - - Ver Aplicaciones soportadas - - - Tus entradas ahora son fácilmente accesibles desde Safari, Chrome y otras aplicaciones soportadas. - - - En Safari y Chrome, busca Bitwarden en el icono compartir (pista: desplazate a la derecha en la última fila del menú). - - - Pulsa en el icono de Bitwarden del menú para iniciar la extensión. - - - Para activar Bitwarden en Safari y otras aplicaciones, pulsa en el icono "más" de la última fila del menú. - - - Favorito - - - Huella dactilar - - - Generar contraseña - - - Obtener pista de la contraseña maestra - - - Importar elementos - - - Puedes importar elementos desde la caja fuerte web de bitwarden.com. ¿Quieres visitar el sitio web ahora? - - - Importa rápida y masivamente elementos desde otras aplicaciones gestoras de contraseñas. - - - Última sincronización: - - - Longitud - - - Bloquear - - - 15 minutos - - - 1 hora - - - 1 minuto - - - 4 horas - - - Inmediatamente - - - Opciones de bloqueo - - - Iniciando sesión... - Message shown when interacting with the server - - - Identifícate o crea una nueva cuenta para acceder a tu caja fuerte. - - - Gestionar - - - La confirmación de la contraseña no es correcta. - - - La contraseña maestra es la clave que utilizas para acceder a tu caja fuerte. Es muy importante que no olvides tu contraseña maestra. No hay forma de recuperarla si la olvidas. - - - Pista de contraseña maestra (opcional) - - - Una pista de tu contraseña maestra puede ayudarte a recordarla en caso de que la olvides. - - - La contraseña maestra debe tener al menos 8 caracteres. - - - Mínimo de caracteres numéricos - Minimum numeric characters for password generator settings - - - Mínimo de caracteres especiales - Minimum special characters for password generator settings - - - Más ajustes - - - Debes identificarte en la aplicación principal de Bitwarden antes de poder uitilizar la extensión. - - - Nunca - - - Nuevo elemento creado. - - - No hay favoritos en tu caja fuerte. - - - No hay elementos en tu caja fuerte. - - - No hay elementos en tu caja fuerte para este sitio web o aplicación. Pulsa para añadir uno. - - - Esta entrada no tiene usuario y contraseña configurados. - - - ¡Entendido! - Confirmation, like "Ok, I understand it" - - - Lo valores por defecto se establecen desde la aplicación principal de Bitwarden en la herramienta de generador de contraseñas. - - - Ajustes - - - Otro - - - Contraseña generada. - - - Generador de contraseñas - - - Pista de contraseña - - - Te hemos enviado un correo electrónico con la pista de tu contraseña maestra. - - - ¿Estás seguro de que quieres sobreescribir la contraseña actual? - - - Bitwarden mantiene tu caja fuerte automáticamente sincronizada utilizando notificaciones push. Para tener la mejor experiencia posible, por favor, pulsa "Permitir" en la próxima notificación donde se te pregunta si quieres habilitar las notificaciones push. - Push notifications for apple products - - - Valora la aplicación - - - ¡Por favor, considera ayudarnos con una buena valoración! - - - Las valoraciones de la App Store son reiniciadas con cada nueva versión de Bitwarden. ¡Por favor, considera apoyarnos con una buena valoración! - - - Regenerar contraseña - - - Vuelve a escribir tu contraseña maestra - - - Buscar en caja fuerte - - - Seguridad - - - Ver progreso del desarrollo - - - Seleccionar - - - Establecer PIN - - - Introduce un PIN de 4 dígitos para desbloquear la aplicación con él. - - - Información del elemento - - - Elemento actualizado. - - - Enviando... - Message shown when interacting with the server - - - Sincronizando... - Message shown when interacting with the server - - - Sincronización completada. - - - Sincronizado fallida. - - - Sincronizar caja fuerte ahora - - - Touch ID - What Apple calls their fingerprint reader. - - - Autenticación en dos pasos - - - La autenticación en dos pasos hace que tu cuenta sea mucho más segura requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web? - - - Desbloquear con {0} - - - Desbloquear con código PIN - - - Validando - Message shown when interacting with the server - - - Código de verificación - - - Ver elemento - - - Caja fuerte web de Bitwarden - - - Gestiona las entradas de cualquier web con la caja fuerte web de Bitwarden. - - - ¿Has perdido la autenticación en dos pasos? - - - Elementos - Screen title - - - ¡Extensión activada! - - - Iconos - - - Traducciones - - - Elementos para {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - No hay elementos en tu caja fuerte para {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Cuando veas una notificación de autorellenado de Bitwarden, puedes pulsar en ella para lanzar el servicio. - - - Pulsa en esta notificación para autorellenar una entrada desde tu caja fuerte. - - - Abrir ajustes de Accesibilidad - - - 1. En los ajustes de Accesibilidad, pulsa en "Bitwarden" bajo el menú Servicios. - - - 2. Activa el interruptor y pulsar en OK para aceptar. - - - Desactivado - - - Activado - - - Estado - - - Beta - - - La forma mas fácil de añadir nuevas entradas a tu caja fuerte es con el Servicio de Autorellenado de Bitwarden. Aprende más sobre como utilizar este servicio yendo a la sección "Herramientas". - - - Autorellenar - - - ¿Quieres autorellenar o ver esta entrada? - - - ¿Estás seguro de que quieres autorellenar esta entrada? No es una coincidencia completa para "{0}". - - - Elementos coincidientes - - - Posibles elementos coincidientes - - - Buscar - - - Estás buscando una entrada para autorellenar "{0}". - - - Comparte tu caja fuerte - - - Crea una organización para compartir tus entradas de forma segura con otros usuarios. - - - Escanear cuando un campo de contraseña está resaltado - - - Solo escanea la pantalla por campos y ofrece una notificación de autorellenado cuando se detecta un campo de contraseña. Esta configuración puede ayudar a conservar batería. - - - Notificación persistente - - - Ofrecer siempre una notificación de autorellenado y solo escanear campos tras intentar el rellenado. Esta opción puede ayudar a conservar batería. - - - Siempre escanear - - - Escanear siempre la pantalla en busca de campos y ofrecer la notificación de autorellenado si se encuentran campos de contraseña. Esta es la opción por defecto. - - - No se puede abrir la aplicación "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplicación de autenticación - For 2FA - - - Introduce el código de verificación de 6 dígitos de tu aplicación autenticadora. - For 2FA - - - Introduce el código de verificación de 6 dígitos que ha sido enviado por correo a {0}. - For 2FA - - - Entrada no disponible - For 2FA whenever there are no available providers on this device. - - - Esta cuenta tiene habilitada la autenticación en dos pasos, pero ninguna de los métodos configurados es soportado por este dispositivo. Por favor, utiliza un dispositivo soportado o/y añade proveedores adicionales que tengan un mejor soporte entre dispositivos (como una aplicación autenticadora). - - - Código de recuperación - For 2FA - - - Recordarme - Remember my two-step login - - - Reenviar código de verificación por correo electrónico - For 2FA - - - Opciones de la autenticación en dos pasos - - - Utilizar otro método de autenticación en dos pasos - - - No se puedo enviar el correo de verificación. Prueba de nuevo. - For 2FA - - - Correo de verificación enviado. - For 2FA - - - Para continuar, mantén tu YubiKey NEO contra la parte trasera de tu dispositivo o inserta tu YubiKey en el puerto USB de tu dispositivo y luego pulsa su butón. - - - Llave de Seguridad YubiKey - "YubiKey" is the product name and should not be translated. - - - Añadir nuevo adjunto - - - Adjuntos - - - No se pudo descargar el archivo. - - - Tu dispositivo no puede abrir este tipo de archivo. - - - Descargando... - Message shown when downloading a file - - - Este adjunto ocupa {0}. ¿Estás seguro de que quieres descargarlo en tu dispositivo? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Clave de autenticación (TOTP) - - - Código de verificación (TOTP) - Totp code label - - - Clave de autenticación añadida. - - - No se pudo leer la clave de autenticación. - - - El escaneo será automático. - - - Apunta tu cámara el código QR. - - - Escanear código QR - - - Cámara - - - Fotos - - - ¡Código TOTP copiado! - - - Copiar código TOTP - - - Si tu entrada tiene una clave de autenticación adjunta, el código de verificación TOTP es copiado automáticamente al portapapeles cuando autorellenas una entrada. - - - Deshabilitar copiado automático de códigos TOTP - - - Se quiere membrasía Premium para poder utilizar esta característica. - - - Adjunto añadido - - - Adjunto eliminado - - - Seleccionar archivo - - - Archivo - - - No se ha elegido ningún archivo - - - No hay adjuntos. - - - Fuente de archivo - - - Característica no disponible - - - El tamaño máximo de archivo es de 100MB. - - - No puedes usar esta característica hasta que actualices tu clave de cifrado. - - - Aprender más - - - URL del servidor de la API - - - Entorno personalizado - - - Para usuarios avanzados. Puedes especificar la URL base de cada servicio de forma independiente. - - - Las URLs del entorno han sido guardadas. - - - {0} no está formateado correctamente. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL del servidor de identidad - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Entorno de alojamiento propio - - - Especifica la URL base de tu instalación de Bitwarden de alojamiento propio. - - - URL del servidor - - - URL del servidor de la caja fuerte web - - - Pulsa en esta notificación para ver las entradas de tu caja fuerte. - - - Campos personalizados - - - Copiar número - - - Copiar código de seguridad - - - Número - - - Código de seguridad - - - ¿Que tipo de elemento quieres añadir? - - - Tarjeta - - - Identidad - - - Entrada - - - Nota segura - - - Dirección 1 - - - Dirección 2 - - - Dirección 3 - - - Abril - - - Agosto - - - Marca - - - Nombre en la tarjeta - - - Ciudad / Pueblo - - - Empresa - - - País - - - Diciembre - - - Dr - - - Mes de expiración - - - Año de expiración - - - Febrero - - - Nombre - - - Enero - - - Julio - - - Junio - - - Apellido - - - Nº de licencia - - - Marzo - - - Mayo - - - 2º nombre - - - Sr - - - Sra - - - Srta - - - Noviembre - - - Octubre - - - Nº de pasaporte - - - Teléfono - - - Septiembre - - - Nº de la seguridad social - - - Estado / Provincia - - - Título - - - Código postal - - - Dirección - - - Expiración - - - Deshabilitar iconos del sitio web - - - Los iconos del sitio web añaden una imagen reconocible al lado de cada entrada de tu caja fuerte. - - - URL del servidor de iconos - - - Autorellenar con Bitwarden - - - Caja fuerte bloqueada - - - Volver a mi caja fuerte - - - Colecciones - - - No hay elementos en esta colección. - - - No hay elementos en esta carpeta. - - - Servicio de accesibilidad de autorellenado - - - El servicio de autorellenado de Bitwarden utiliza el marco de autocompletado de Android para ayudarte a rellenar entradas, tarjetas de crédito e identidades en otras aplicaciones de tu dispositivo. - - - Utiliza el servicio de accesibilidad de Bitwarden para autorellenar entradas. - - - Abrir opciones de autorellenado - - - Face ID - What Apple calls their facial recognition reader. - - - Usar Face ID para verificar. - - - Usar Face ID para desbloquear - - - Verificar Face ID - - - Desbloquear con Windows Hello - - - Verificar con Windows Hello - - - Windows Hello - - - No hemos podido abrir automáticamente las opciones de autorellenado de Android. Puedes ir al menú de opciones de autorellenado manualmente desde Ajustes de Android > Sistema > Idiomas y entradas > Avanzado > Servicio autocompletar. - - - Nombre del campo personalizado - - - Booleano - - - Oculto - - - Texto - - - Nuevo campo personalizado - - - ¿Que tipo de campo personalizado quieres añadir? - - - Eliminar - - - Nueva URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Dominio base - - - Por defecto - - - Exacta - - - Servidor - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Expresión regular - A programming term, also known as 'RegEx'. - - - Empieza con - - - Tipo de detección de URI - - - Tipo de detección - URI match detection for auto-fill. - - - Sí y guardar - - - Autorellenar y guardar - - - Organización - An entity of multiple related people (ex. a team or business organization). - - - Mantén tu Yubikey cerca de la parte superior del dispositivo. - - - Vuelve a intentarlo - - - Para continuar, mantén tu YubiKey NEO contra la parte trasera de tu dispositivo. - - - El servicio de accesibilidad puede ser útil para utilizarlo con aplicaciones que no soportan el sistema de autorellenado estándar. - - - Contraseña actualizada - ex. Date this password was updated - - - Actualizada - ex. Date this item was updated - - - ¡Autorellenado activado! - - - Debes identificarte en la aplicación principal de Bitwarden antes de poder utilizar Autorellenado. - - - Tus entradas son ahora fácilmente accesibles desde tu teclado mientras te identificas en aplicaciones y sitios web. - - - Te recomendamos deshabilitar cualquier otra aplicación de Autorellenado desde Ajustes si no piensas utilizarlas. - - - Accede a tu caja fuerte directamente desde tu teclado para autorellenar contraseñas rápidamente. - - - Para habilitar el Autorellenado de contraseña en su dispositivo, sigue estas instrucciones: - - - 1. Ve a la aplicación "Ajustes" de iOS - - - 2. Pulsa en "Contraseñas y Cuentas" - - - 3. Pulsa en "Autorellenado de contraseñas" - - - 4. Habilita el autorellenado - - - 5. Selecciona "Bitwarden" - - - Autorellenado de contraseña - - - La forma más facil de añadir nuevas entradas a tu caja fuerte es usando la extensión de Autorellenado de contraseñas Bitwarden. Aprende más sobre como utilizar la extensión de Autorellenado de contraseñas Bitwarden yendo a la pantalla de Herramientas. - - diff --git a/src/App/Resources/AppResources.et.resx b/src/App/Resources/AppResources.et.resx deleted file mode 100644 index ed9af0f92..000000000 --- a/src/App/Resources/AppResources.et.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Rakenduse info - - - Lisa - Add/create a new entity (verb). - - - Kausta lisamine - - - Kirje lisamine - The title for the add item page. - - - Ilmnes tõrge. - Alert title when something goes wrong. - - - Tagasi - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Tühista - Cancel an operation. - - - Kopeeri - Copy some value to your clipboard. - - - Kopeeri parool - The button text that allows a user to copy the login's password to their clipboard. - - - Kopeeri kasutajanimi - The button text that allows a user to copy the login's username to their clipboard. - - - Tänusõnad - Title for page that we use to give credit to resources that we use. - - - Kustuta - Delete an entity (verb). - - - Kustutamine... - Message shown when interacting with the server - - - Kas soovid kindlasti kustutada? Seda ei saa tagasi võtta. - Confirmation alert message when deleteing something. - - - Muuda - - - Muuda kausta - - - E-post - Short label for an email address. - - - E-posti aadress - Full label for a email address. - - - Kirjuta meile - - - Kirjuta meile abi saamiseks või tagasiside jätmiseks. - - - Sisesta PIN kood. - - - Lemmikud - Title for your favorite items in the vault. - - - Esita tarkvaraviga - - - Ava meie GitHubis probleemi kirjeldus. - - - Kasuta lahtilukustamiseks sõrmejälge. - - - Kaust - Label for a folder. - - - Uus kaust on loodud. - - - Kaust on kustutatud. - - - Kaust puudub - Items that have no folder specified go in this special "catch-all" folder. - - - Kaustad - - - Kaust on uuendatud. - - - Külasta veebisaiti - The button text that allows user to launch the website to their web browser. - - - Abi ja tagasiside - - - Peida - Hide a secret value that is currently shown (password). - - - Jätkamiseks on vajalik internetiühendus. - Description message for the alert when internet connection is required to continue. - - - Vajalik on internetiühendus - Title for the alert when internet connection is required to continue. - - - Vale ülemparool. Proovi uuesti. - - - Vale PIN. Proovi uuesti. - - - Käivita - The button text that allows user to launch the website to their web browser. - - - Logi sisse - The login button text (verb). - - - Sisselogimine - Title for login page. (noun) - - - Logi välja - The log out button text (verb). - - - Oled kindel, et soovid välja logida? - - - Ülemparool - Label for a master password. - - - Rohkem - Text to define that there are more options things to see. - - - Minu hoidla - The title for the vault page. - - - Nimi - Label for an entity name. - - - Ei - - - Märkmed - Label for notes. - - - Ok - Acknowledgement. - - - Parool - Label for a password. - - - Salvesta - Button text for a save operation (verb). - - - Salvestan... - Message shown when interacting with the server - - - Seaded - The title for the settings page. - - - Näita - Reveal a hidden value (password). - - - Kirje on kustutatud. - Confirmation message after successfully deleting a login. - - - Esita - - - Sünkroniseeri - The title for the sync page. - - - Täname teid! - - - Tööriistad - The title for the tools page. - - - URL - Label for a uri/url. - - - Kasuta lahtilukustamiseks sõrmejälge - - - Kasutajanimi - Label for a username. - - - Väli {0} on kohustuslik. - Validation message for when a form field is left blank and is required to be entered. - - - {0} on kopeeritud. - Confirmation message after suceessfully copying a value to the clipboard. - - - Kinnita sõrmejäljega - - - Kinnita ülemparooliga - - - Kinnita PIN - - - Versioon - - - Vaata - - - Külasta meie kodulehte - - - Külasta kodulehte abi saamiseks, meiega kontakteerumiseks, uudiste lugemiseks või Bitwardeni kohta rohkema teabe saamiseks. - - - Veebisait - Label for a website. - - - Jah - - - Konto - - - Sinu konto on loodud! Võid nüüd sisse logida. - - - Lisa kirje - - - Rakenduse laiendus - - - Kasuta Bitwardeni juurdepääsetavuse teenust, et erinevatel veebilehtedel ja rakendustes kontoandmeid sisestada. - - - Automaattäite teenus - - - Väldi ebamääraseid kirjamärke - - - Bitwarden rakenduse laiendus - - - Lihtsaim viis uute sisselogimise andmete lisamiseks on kasutada Bitwardeni rakenduse laiendust. Lisateavet Bitwardeni rakenduse laienduse kasutamiseks leiad menüüst "Tööriistad". - - - Kasuta Bitwardenit Safaris ja teistes rakendusestes kontoandmete sisestamiseks. - - - Bitwardeni automaattäite teenus - - - Kasuta kontoandmete sisestamiseks Bitwardeni juurdepääsetavuse teenust. - - - Muuda e-posti - - - Saad oma e-posti aadressi muuta bitwarden.com veebihoidlas. Soovid seda kohe teha? - - - Muuda ülemparooli - - - Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha? - - - Sulge - - - Peagi lisandumas! - - - Jätka - - - Kopeeritud! - - - Parool on kopeeritud! - - - Kasutajanimi on kopeeritud! - - - Loo konto - - - Konto loomine... - Message shown when interacting with the server - - - Kirje muutmine - - - Aktiveeri automaatne sünkroonimine - - - Ülemparooli vihje saamiseks sisesta oma konto e-posti aadress. - - - Taas-aktiveeri rakenduse laiendus - - - Peaaegu valmis! - - - Aktiveeri rakenduse laiendus - - - Safaris pääsed Bitwardenile ligi läbi jagamise ikooni (vihje: vaata jagamise menüüs paremale-alla). - Safari is the name of apple's web browser - - - Kiire ligipääs oma paroolidele! - - - Oled sisselogimiseks valmis! - - - Vaata toetatud rakendusi - - - Pääsed nüüd oma kontoandmetele lihtsasti ligi nii Safaris, Chromes kui ka teistes toetatud rakendustes. - - - Safaris ja Chromes pääsed Bitwardenile ligi läbi jagamise ikooni (vihje: vaata jagamise menüüs paremale-alla). - - - Lisa käivitamiseks vajuta menüüs Bitwardeni ikoonile. - - - Bitwardeni sisselülitamiseks Safaris ja teistes rakendustes vajuta alumisel menüül "more" ikoonile. - - - Lemmik - - - sõrmejäljega - - - Loo parool - - - Tuleta ülemparooli vihjega meelde - - - Impordi andmed - - - Saad suurema koguse kontoandmeid importida Bitwardeni veebihoidlas. Soovid seda kohe teha? - - - Kiire viis oma konto- ja muude andmete importimiseks teistest paroolihalduritest. - - - Viimane sünkronisatsioon: - - - Pikkus - - - Lukusta - - - 15 minuti pärast - - - 1 tunni pärast - - - 1 minuti pärast - - - 4 tunni pärast - - - Koheselt - - - Automaatne lukustamine - - - Sisselogimine... - Message shown when interacting with the server - - - Logi oma olemasolevasse kontosse sisse või loo uus konto. - - - Halda - - - Parooli kinnitus ei ole õige. - - - Ülemparool on parool, millega pääsed oma kontole ligi. On äärmiselt tähtis, et ülemparool ei ununeks. Selle parooli taastamine ei ole mingil moel võimalik. - - - Ülemparooli vihje (ei ole kohutuslik) - - - Vihje võib abiks olla olukorras, kui oled ülemparooli unustanud. - - - Ülemparool peab olema vähemalt 8 tähemärgi pikkune. - - - Vähim arv numbreid - Minimum numeric characters for password generator settings - - - Vähim arv spetsiaalmärke - Minimum special characters for password generator settings - - - Rohkem seadeid - - - Pead enne lisarakenduse kasutamist Bitwardeni rakendusse sisse logima. - - - Mitte kunagi - - - Uus kirje on loodud. - - - Hoidlas ei ole lemmikuid. - - - Hoidlas ei ole kirjeid. - - - Hoidlas puudub selle veebilehe konto. Vajuta konto lisamiseks. - - - Selle konto jaoks on kasutajanimi või parool seadistamata. - - - Ok, sain aru! - Confirmation, like "Ok, I understand it" - - - Vaikeväärtused on seatud Bitwardeni paroolide genereerimise tööriista poolt. - - - Valikud - - - Muu - - - Parool on loodud. - - - Parooli genereerimine - - - Parooli vihje - - - Ülemparooli vihje saadeti Sinu e-postile. - - - Oled kindel, et soovid olemasolevat parooli asendada? - - - Bitwarden hoiab hoidla automaatselt sünkroonis läbi push teavituste. Parima võimaliku kogemuse saamiseks vajuta järgnevas märkekastis "Allow", et sünkroniseerimine lubada. - Push notifications for apple products - - - Hinda rakendust - - - Soovi korral võid meid positiivse hinnanguga toetada! - - - App Store reitingud nullitakse iga uue Bitwardeni versiooniga. Palume Sul võimalusel meie rakendust hea hinnanguga toetada! - - - Genereeri salasõna uuesti - - - Sisesta ülemparool uuesti - - - Otsi hoidlast - - - Turvalisus - - - Vaata arenduse edenemist - - - Vali - - - Määra PIN - - - Kasuta lahtilukustamiseks 4 numbrilist koodi. - - - Kirje teave - - - Kirje on uuendatud. - - - Saatmine... - Message shown when interacting with the server - - - Sünkroniseerimine... - Message shown when interacting with the server - - - Sünkroniseerimine on lõpetatud. - - - Sünkroniseerimine nurjus. - - - Sünkroniseeri hoidla - - - Touch ID - What Apple calls their fingerprint reader. - - - Kaheastmeline kinnitamine - - - Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada? - - - Lukusta lahti {0} - - - Lukusta lahti PIN koodiga - - - Valideerimine - Message shown when interacting with the server - - - Kinnituskood - - - Kirje vaatamine - - - Bitwardeni Veebihoidla - - - Halda oma Bitwardeni kontot ja selle sisu mistahes brauseris. - - - Kaotasid autentiseerimise rakenduse? - - - Kirjet - Screen title - - - Lisa on aktiveeritud! - - - Ikoonid - - - Tõlkijad - - - {0} kontoandmed - This is used for the autofill service. ex. "Logins for twitter.com" - - - Hoidlas puuduvad {0} kontod. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Kui näed Bitwardeni automaatse täitmise märguannet, saad sellele vajutada ja oma kontoandmed automaatselt sisestada. - - - Vajuta andmete automaatseks sisestamiseks. - - - Ava Juurdepääsetavuse seaded - - - 1. Vajuta Androidi Juurdepääsetavuse seadete menüüs "Bitwardeni" kirjele. - - - 2. Lülita valik sisse ja seejärel OK, et sellega nõustuda. - - - Keelatud - - - Lubatud - - - Olek: - - - Beeta - - - Lihtsaim viis hoidlasse kontosid lisada on läbi Bitwardeni automaattäite teenuse. Rohkem infot selle kohta leiad Bitwardeni rakenduse alammenüüst "Tööriistad". - - - Automaattäide - - - Soovid seda kirjet sisestada või vaadata? - - - Oled kindel, et soovid selle kirje automaatselt sisestada? See ei ühti täielikult "{0}"-ga. - - - Sobivad kirjed - - - Võimalikud sobivad kirjed - - - Otsi - - - Otsid "{0}" automaattäite kirjet. - - - Jaga oma hoidlat - - - Loo organisatsioon, et saaksid oma andmeid teiste inimestega turvaliselt jagada. - - - Paku täitmist, kui valitud on parooliväli - - - Otsi ekraanilt täitmiseks mõeldud välju ning näita automaattäite teavitust ainult siis, kui valitud on parooli väli. See valik võib aidata akut säästa. - - - Püsiv märguanne - - - Paku alati automaattäite märguannet ning kontrolli välju alles pärast automaatse täitmise valiku kasutamist. See valik võib aidata akut säästa. - - - Otsi alati - - - Otsi ekraanilt sisselogimise välju ning näita automaattäite teavitust paroolivälja leidmisel. See on vaikeseade. - - - Rakendust "{0}" ei ole võimalik avada. - Message shown when trying to launch an app that does not exist on the user's device. - - - Autentiseerimise rakendus - For 2FA - - - Sisesta autentiseerimise rakendusest 6 kohaline number. - For 2FA - - - Sisesta 6 kohaline number, mis saadeti e-posti aadressile {0}. - For 2FA - - - Sisselogimine ei ole saadaval - For 2FA whenever there are no available providers on this device. - - - Sellel kontol on aktiveeritud kaheastmeline kinnitus. Siiski ei toeta ükski aktiveeritud kaheastmelise kinnitamise teenus seda konkreetset seadet. Palun kasuta ühilduvat seadet ja/või lisa uus kaheastmelise teenuse pakkuja, mis töötab rohkemates seadmetes (näiteks mõni autentiseerimise rakendus). - - - Taastamise kood - For 2FA - - - Jäta mind meelde - Remember my two-step login - - - Saada kinnituskood uuesti e-postile - For 2FA - - - Kaheastmelise sisselogimise valikud - - - Kasuta teist kaheastmelist sisselogimise meetodit - - - Kinnitus e-kirja saatmine ebaõnnestus. Proovi uuesti. - For 2FA - - - Kinnitus e-kiri on saadetud. - For 2FA - - - Jätkamiseks hoia oma YubiKey NEO-d vastu seadet või sisesta see seadme USB pessa. Seejärel vajuta sellele nupule. - - - YubiKey Security Key - "YubiKey" is the product name and should not be translated. - - - Lisa uus manus - - - Manused - - - Faili allalaadimine nurjus. - - - Sinu seade ei oska seda faili avada. - - - Allalaadimine... - Message shown when downloading a file - - - See manus on {0} suurusega. Oled kindel, et soovid seda oma seadmesse allalaadida? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Autentiseerimise võti (TOTP) - - - Kinnituskood (TOTP) - Totp code label - - - Autentiseerimise võti on lisatud. - - - Autentiseerimise võtme lugemine nurjus. - - - Skaneering toimub automaatselt. - - - Suuna telefoni kaamera QR koodile. - - - Skanneeri QR kood - - - Kaamera - - - Fotod - - - TOTP kopeeritud! - - - Kopeeri TOTP - - - Kui sinu sisselogimise andmetele on juurde lisatud autentiseerimise võti, kopeeritakse TOTP kood automaattäite kasutamisel lõikepuhvrisse. - - - Keela automaatne TOTP kopeerimine - - - Selle funktsiooni kasutamiseks on vajalik tasulist kontot omada. - - - Manus on lisatud - - - Manus on kustutatud - - - Vali fail - - - Fail - - - Faili pole valitud - - - Manused puuduvad. - - - Faili allikas - - - Funktsioon pole saadaval - - - Maksimaalne faili suurus on 100 MB. - - - Seda funktsiooni ei saa enne krüpteerimise võtme uuendamist kasutada. - - - Rohkem teavet - - - API serveri URL - - - Kohandatud keskkond - - - For advanced users. You can specify the base URL of each service independently. - - - The environment URLs have been saved. - - - {0} ei ole õige formaadiga. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identity Server URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Self-hosted Environment - - - Specify the base URL of your on-premise hosted Bitwarden installation. - - - Serveri URL - - - Web Vault Server URL - - - Vajuta märguandele, et oma hoidla sisu näha. - - - Kohandatud väljad - - - Kopeeri number - - - Kopeeri turvakood - - - Number - - - Turvakood - - - Millist tüüpi kirjet soovid lisada? - - - Pangakaart - - - Identiteet - - - Kasutajakonto andmed - - - Turvaline märkus - - - Aadress 1 - - - Aadress 2 - - - Aadress 3 - - - Aprill - - - August - - - Väljastaja - - - Kaardiomaniku nimi - - - Linn / asula - - - Ettevõte - - - Riik - - - Detsember - - - Dr - - - Aegumise kuu - - - Aegumise aasta - - - Veebruar - - - Eesnimi - - - Jaanuar - - - Juuli - - - Juuni - - - Perekonnanimi - - - Litsentsi number - - - Märts - - - Mai - - - Teine eesnimi - - - Hr - - - Mrs - - - Pr - - - November - - - Oktoober - - - Passi number - - - Telefoninumber - - - September - - - Isikukood - - - Maakond / vald - - - Pealkiri - - - Postiindeks - - - Aadress - - - Aegumine - - - Keela veebilehel ikoonid - - - Ikoonid aitavad hoidlas olevaid veebilehti paremini ära tunda. - - - Ikoonide serveri URL - - - Täida automaatselt Bitwardeniga - - - Hoidla on lukus - - - Ava hoidla - - - Kogumikud - - - Selles kogumikus ei ole kirjeid. - - - Selles kaustas ei ole kirjeid. - - - Automaattäite Juurdepääsetavuse teenus - - - Bitwardeni automaattäite teenus kasutab Androidi Autofill raamistikku. See aitab telefonis olevatesse rakendustesse sisestada kontoandmeid, krediitkaardi andmeid ja muud personaalset infot. - - - Kasuta Bitwardeni automaattäite teenust, et sisestada teistesse rakendustesse kontoandmeid, krediitkaardi andmeid ja muud informatsiooni. - - - Ava Automaattäite seaded - - - Face ID - What Apple calls their facial recognition reader. - - - Kasuta kinnitamiseks Face ID-d. - - - Kasuta lahtilukustamiseks Face ID-d - - - Kinnita Face ID - - - Lukusta lahti Windows Helloga - - - Kinnita Windows Helloga - - - Windows Hello - - - Meil ei õnnestu Androidi sisestusabi seadeid avada. Võid selle ise avada, navigeerides Seaded > Süsteem > Keeled ja sisend > Täpsemad > Sisestusabi. - - - Kohandatud välja nimi - - - Boolean - - - Peidetud - - - Tekst - - - Uus kohandatud väli - - - Millist tüüpi kohandatud välja soovid lisada? - - - Eemalda - - - Uus URL - - - URL {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Baasdomeen - - - Vaike - - - Täpne - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - RegEx - A programming term, also known as 'RegEx'. - - - Algab - - - URL sobivuse tuvastamine - - - Sobivuse tuvastamine - URI match detection for auto-fill. - - - Jah, ja salvesta - - - Täida ja salvesta - - - Organisatsioon - An entity of multiple related people (ex. a team or business organization). - - - Hoia oma Yubikey seadme ülaosa ligidal. - - - Proovi uuesti - - - Jätkamiseks hoia YubiKey NEO-d oma seadme tagumise poole vastu. - - - Juurdepääsetavuse teenus võib abiks olla olukordades, kus rakendused ei toeta standardset automaattäite teenust. - - - Parool on uuendatud - ex. Date this password was updated - - - Uuendatud - ex. Date this item was updated - - - Automaattäide on aktiveeritud! - - - Pead enne Automaattäite kasutamist Bitwardeni rakendusse sisse logima. - - - Sinu kontoandmed on otse klaviatuurilt ligipääsetavad. Saad nüüd kergemini äppides ja veebilehtel sisse logida. - - - Soovitame teised Automaattäite valikud Seadetest välja lülitada, kui sa neid enam ei kasuta. - - - Pääse oma hoidlale otse klaviatuurilt ligi ning täida parooliväljad lihtsamini. - - - Paroolide automaattäite võimaldamiseks järgi neid juhiseid: - - - 1. Mine iOS "Settings" rakendusse - - - 2. Vajuta "Passwords & Accounts" - - - 3. Vajuta "AutoFill Passwords" - - - 4. Lülita AutoFill sisse - - - 5. Vali "Bitwarden" - - - Paroolide Automaatne täitmine - - - Kõige lihtsam viis uute kontoandmete sisestamiseks on kasutada Bitwardeni Automaattäite lisatööriista. Rohkem teavet selle kasutamise kohta leiad "Tööriistad" menüüst. - - diff --git a/src/App/Resources/AppResources.fa.resx b/src/App/Resources/AppResources.fa.resx deleted file mode 100644 index 5615384fe..000000000 --- a/src/App/Resources/AppResources.fa.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - درباره ما - - - افزودن - Add/create a new entity (verb). - - - افزودن پوشه - - - افزودن مورد - The title for the add item page. - - - خطایی رخ داده است. - Alert title when something goes wrong. - - - بازگشت - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - انصراف - Cancel an operation. - - - کپی - Copy some value to your clipboard. - - - کپی رمز عبور - The button text that allows a user to copy the login's password to their clipboard. - - - کپی نام کاربری - The button text that allows a user to copy the login's username to their clipboard. - - - پدید آورندگان - Title for page that we use to give credit to resources that we use. - - - حذف - Delete an entity (verb). - - - در حال حذف... - Message shown when interacting with the server - - - مطمئنید میخواهید حذف کنید؟ این عمل قابل بازگشت نیست. - Confirmation alert message when deleteing something. - - - ویرایش - - - ويرايش پوشه - - - ایمیل - Short label for an email address. - - - آدرس ایمیل - Full label for a email address. - - - ایمیل ما - - - برای کمک گرفتن و گذاشتن بازخورد بصورت مستقیم به ما ایمیل ارسال کنید. - - - پین کد را وارد کنید. - - - علاقه مندی ها - Title for your favorite items in the vault. - - - گزارش یک مشکل - - - یک مسئله را در مخزن GitHub ما باز کنید. - - - از اثر انگشت برای تأیید استفاده کنید. - - - پوشه - Label for a folder. - - - پوشه جدید ساخته شد. - - - پوشه حذف شد. - - - بدون پوشه - Items that have no folder specified go in this special "catch-all" folder. - - - پوشه ها - - - پوشه بروزرسانی شد. - - - برو به وب سایت - The button text that allows user to launch the website to their web browser. - - - کمک و باز خورد - - - پنهان کردن - Hide a secret value that is currently shown (password). - - - لطفا قبل از ادامه به اینترنت متصل شوید. - Description message for the alert when internet connection is required to continue. - - - اتصال به اینترنت ضروریست - Title for the alert when internet connection is required to continue. - - - گذرواژه اصلی نامعتبر است. دوباره امتحان کنید. - - - پین نامعتبر است. دوباره امتحان کنید. - - - راه اندازی - The button text that allows user to launch the website to their web browser. - - - ورود - The login button text (verb). - - - ورود - Title for login page. (noun) - - - خروج - The log out button text (verb). - - - آیا مطمئنید که میخواهید خارج شوید؟ - - - گذرواژه‌ ی اصلی - Label for a master password. - - - بیشتر - Text to define that there are more options things to see. - - - گاوصندوق من - The title for the vault page. - - - نام - Label for an entity name. - - - خیر - - - یادداشت ها - Label for notes. - - - تأیید - Acknowledgement. - - - رمز عبور - Label for a password. - - - ذخیره - Button text for a save operation (verb). - - - درحال ذخیره... - Message shown when interacting with the server - - - تنظیمات - The title for the settings page. - - - نمایش - Reveal a hidden value (password). - - - مورد حذف شد. - Confirmation message after successfully deleting a login. - - - ثبت - - - همگام سازی - The title for the sync page. - - - با تشکر از - - - ابزارها - The title for the tools page. - - - آدرس اینترنتی - Label for a uri/url. - - - از اثر انگشت برای بازگشایی قفل استفاده کنید - - - نام کاربری - Label for a username. - - - فیلد {0} ضروریست. - Validation message for when a form field is left blank and is required to be entered. - - - {0} کپی شد. - Confirmation message after suceessfully copying a value to the clipboard. - - - تأیید اثر انگشت - - - تأیید گذرواژه اصلی - - - تأیید پین - - - نسخه - - - مشاهده - - - از وب‌ سایت ما دیدن کنید - - - برای کمک گرفتن، اخبار، ایمیل ما، و یا آموزش بیشتر برای استفاده از Bitwarden از وب سایت ما دیدن کنید. - - - وب سایت - Label for a website. - - - بله - - - حساب - - - حساب کاربری شما ساخته شد! حالا میتوانید وارد شوید. - - - افزودن یک مورد - - - افزونه اپلیکیشن - - - از قابلیت دسترسی Bitwarden برای پر کردن خودکار ورود به سیستم خود در سراسر برنامه ها و وب استفاده کنید. - - - سرویس پر کردن خودکار - - - از تکرار کاراکترهای یک شکل اجتناب کن - - - افزونه اپلیکیشن Bitwarden - - - ساده ترین روش برای افزودن اطلاعات ورود به گاوصندوق از طریق افزونه اپلیکیشن Bitwarden است. برای اطلاع بیشتر از چگونگی استفاده از افزونه اپلیکیشن مرورکردن صفحه "ابزار" است. - - - با Bitwarden در سافاری و دیگر برنامه ها میتوانید از قابلیت پر کردن خودکار در ورودهایتان استفاده کنید. - - - سرویس پرکردن خودکار Bitwarden - - - از قابلیت دسترسی Bitwarden برای تکمیل خودکار ورود به سیستم خود در سراسر برنامه ها و وب استفاده کنید. - - - تغییر ایمیل - - - شما میتوانید آدرس ایمیل را در نسخه وب گاوصندوق bitwarden.com تغییر دهید. میخواهید اکنون از سایت بازدید کنید؟ - - - تغییر گذرواژه اصلی - - - شما می توانید گذرواژه اصلی خود را در bitwarden.com تغییر دهید. آیا میخواهید از سایت بازدید کنید؟ - - - بستن - - - به زودی می آید! - - - ادامه - - - کپی انجام شد! - - - رمز عبور کپی شد! - - - نام کاربری کپی شد! - - - ایجاد حساب کاربری - - - در حال ایجاد حساب کاربری... - Message shown when interacting with the server - - - ویرایش مورد - - - فعال کردن همگام سازی خودکار - - - برای دریافت راهنمایی گذرواژه اصلی خود آدرس ایملیتان را وارد کنید. - - - افزونه اپلیکیشن را دوباره فعال کنید - - - تقریباً تمام شد! - - - افزونه اپلیکیشن را فعال کنید - - - در سافاری، Bitwarden را با استفاده از آیکون اشتراک گذاری پیدا کنید (راهنمایی: به سمت راست در ردیف پایین فهرست بروید). - Safari is the name of apple's web browser - - - دسترسی سریع به گذرواژه هایتان را دریافت کنید! - - - برای ورود آماده اید! - - - مشاهده برنامه های پشتیبانی شده - - - اطلاعات ورود شما حالا از سافاری، کروم، و دیگر برنامه های پشتیبانی شده به راحتی قابل دسترسی است. - - - در سافاری و کروم، Bitwarden را با استفاده از آیکون اشتراک گذاری پیدا کنید (راهنمایی: به سمت راست در ردیف پایین فهرست بروید). - - - برای راه اندازی افزونه به آیکون Bitwarden در منو ضربه بزنید. - - - برای فعال سازی Bitwarden در سافاری و سایر برنامه ها، نماد "بیشتر" را در پایین ردیف منو ضربه بزنید. - - - مورد علاقه - - - اثر انگشت - - - تولید رمز عبور - - - دریافت راهنمای گذرواژه اصلی - - - واردن کردن موارد - - - شما میتوانید موارد را بصورت فله ای در گاوصندوق وب bitwarden.com وارد کنید. آیا مایل به دیدن وبسایت هستید؟ - - - موارد مهم خود را سریعاً از دیگر برنامه های مدیریت رمز عبور وارد کنید. - - - آخرین همگام سازی: - - - طول - - - قفل - - - ۱۵ دقیقه - - - ۱ ساعت - - - ۱ دقیقه - - - ۴ ساعت - - - بلافاصله - - - گزینه های قفل - - - در حال ورود... - Message shown when interacting with the server - - - وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امنتان دسترسی یابید. - - - مدیریت - - - تأیید رمز عبور صحیح نیست. - - - گذرواژه اصلی رمز عبوری است که شما برای دسترسی به گاوصندوق خود استفاده میکنید. بیاد داشتن گذرواژه اصلی بسیار اهمیت دارد. اگر فراموشش کنید هیچ راهی برای بازگردانی آن وجود ندارد. - - - راهنمای گذرواژه اصلی (اختیاری) - - - راهنمای گذرواژه اصلی می تواند در صورت فراموشی آن را بیاد بیارید. - - - طول گذرواژه اصلی باید حداقل ۸ کاراکتر باشد. - - - حداقل اعداد - Minimum numeric characters for password generator settings - - - حداقل حرف خاص - Minimum special characters for password generator settings - - - تنظیمات بیشتر - - - شما بایستی به برنامه اصلی Bitwarden وارد شوید تا بتوانید از افزونه استفاده کنید. - - - هرگز - - - مورد جدید ساخته شد. - - - هیج علاقه مندی در گاوصندوقتان موجود نیست. - - - هیج موردی در گاوصندوقتان موجود نیست. - - - هیج علاقه مندی برای این وب سایت در گاوصندوقتان موجود نیست. برای افزودن ضربه بزنید. - - - این ورود هیچ نام کاربری و رمز عبور پیکربندی شده ای ندارد. - - - باشه، فهمیدم! - Confirmation, like "Ok, I understand it" - - - گزینه های پیش فرض از ابزار تولید رمز عبور Bitwarden تنظیم شد. - - - گزینه ها - - - ساير - - - رمز عبور تولید شد. - - - تولید کننده رمز عبور - - - راهنمای گذرواژه - - - ما یک ایمیل همراه با راهنمای گذرواژه اصلی برایتان ارسال کردیم. - - - آیا از بازنویسی بر روی رمز عبور فعلی مطمئن هستید؟ - - - Bitwarden گاوصندوق شما را بصورت خودکار از طریق نشان دادن اطلاعیه ها به هنگام سازی میکند. برای بهترین تجربه کاربری ممکن، لطفاً زمانی که از شما پرسیده شد "مجاز" را سریعاً انتخاب کنید تا نمایش اعلانیه ها فعال شود. - Push notifications for apple products - - - به برنامه امتیاز دهید - - - لطفاً با یک بررسی خوب به ما کمک کنید! - - - رتبه بندی فروشگاه App Store با هر نسخه جدید از Bitwarden تنظیم می شود. لطفاً با بررسی خوب به ما کمک کنید! - - - تولید رمز عبور - - - تایپ دوباره گذرواژه اصلی - - - جستجوی گاوصندوق - - - امنیت - - - دیدن پیشرفت و توسعه - - - انتخاب - - - تعیین پین - - - یک پین کد 4 رقمی برای باز کردن برنامه وارد کنید. - - - اطلاعات مورد - - - موارد بروزرسانی شد. - - - در حال ارسال... - Message shown when interacting with the server - - - در حال همگام سازی... - Message shown when interacting with the server - - - همگام سازی کامل شد. - - - همگام سازی شکست خورد. - - - همگام سازی گاو صندوق - - - Touch ID - What Apple calls their fingerprint reader. - - - ورود دو مرحله ای - - - ورود دو مرحله ای باعث می شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه تأیید هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا میخواهید از سایت بازدید کنید؟ - - - باز کردن با {0} - - - باز کردن با پین کد - - - اعتبار سنجی - Message shown when interacting with the server - - - کد تأیید - - - مشاهده مورد - - - گاوصندوق وب Bitwarden - - - مورد های خود را از هر مرورگر وب با گاوصندوق وب Bitwarden مدیریت کنید. - - - برنامه تأیید کننده از دست رفته؟ - - - موارد - Screen title - - - افزونه فعال شد! - - - آیکن‌ ها - - - ترجمه ها - - - موارد برای {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - هیج موردی در گاوصندوقتان برای {0} موجود نیست. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - زمانی که شما یک اطلاع رسانی پر کردن خودکار Bitwarden را میبینید، میتوانید برای اجرای پر کردن خودکار ضربه بزنید. - - - برای پر کردن خودکار یک اطلاع رسانی در گاوصندوقتان این اطلاع رسانی را ضربه بزنید. - - - باز کردن تنظیمات دستیابی - - - 1. در صفحه تنظیمات دستیابی اندروید، "Bitwarden" را در قسمت سرویس ها لمس کنید. - - - ۲. روشن کردن را انتخاب کنید و تأیید را بفشارید. - - - غیرفعال شد - - - فعال شد - - - وضعیت - - - آزمایشی - - - ساده ترین روش برای افزودن اطلاعات ورود به گاوصندوقتان از سرویس پر کردن خودکار Bitwarden است. برای اطلاعات بیشتر درباره استفاده از سرویس پر کردن خودکار Bitwarden است مرور کردن صفحه "ابزار" است. - - - پر کردن خودکار - - - آیا مایل به پر کردن خودکار یا دیدن این مورد هستید؟ - - - آیا از پر کردن خودکار این مورد اطمینان دارید؟ کاملا منطبق با "{0}" نیست. - - - تطبیق موارد - - - تطبیق موارد ممکن است - - - جستجو - - - شما به دنبال یافتن یک پر کردن خودکار برای "{0}" هستید. - - - اشتراک گذاری گاوصندوقتان - - - ایجاد یک تشکیلات برای اشتراک گذاری ایمن موارد خود با سایر کاربران. - - - اسکن زمانی که در فیلد پسورد متمرکز شده - - - فقط صفحه را برای فیلدها و پیشنهاد دادن پر کردن خودکار اسکن میکند هر حتی اگر شما یک فیلد رمز عبور را انتخاب کرده باشید. این تنظیم به افزایش عمر باتری کمک میکند. - - - اطلاعیه ثابت - - - همیشه یک پر کردن خودکار را اطلاع میدهد و فقط بعد از تلاش برای پر کردن خودکار فیلدها اقدام به اسکن میکند. این تنظیم ممکن است در صرفه جویی باتری موثر باشد. - - - همیشه اسکن کن - - - اگر فیلد رمز عبور یافت شود همیشه صفحه را برای فیلدها و پیشنهاد اعلانیه یک پر کردن خودکار اسکن میکند. این تنظیم پیش فرض است. - - - نمیتوان برنامه را باز کرد "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - برنامه تأیید کننده - For 2FA - - - کد ۶ رقمی تأیید را از برنامه تأیید کننده وارد کنید. - For 2FA - - - کد ۶ رقمی تأیید را که به {0} ایمیل شده را وارد کنید. - For 2FA - - - ورود به سیستم موجود نیست - For 2FA whenever there are no available providers on this device. - - - ورود دو مرحله ای برای این حساب فعال است، هرچند که، هیچ یک از ارائه دهندگان دو مرحله ای پیکربندی شده در این دستگاه پشتیبانی نمی شود. لطفاً از یک دستگاه پشتیبانی شده استفاده کنید یا از یک ارائه دهنده اضافه کنید که بهتر از سایر دستگاه ها پشتیبانی می کند (مانند یک برنامه تأیید هویت). - - - کد بازیابی - For 2FA - - - مرا به خاطر بسپار - Remember my two-step login - - - ارسال دوباره ایمیل کد تأیید - For 2FA - - - گزینه های ورود دو مرحله ای - - - استفاده از روش ورود دو مرحله ای دیگر - - - نمیتوان ایمیل تأیید را ارسال کرد. دوباره امتحان کنید. - For 2FA - - - ایمیل تأیید فرستاده شد. - For 2FA - - - برای ادامه، YubiKey NEO خود را در پشت دستگاه نگه دارید یا YubiKey خود را در پورت USB دستگاه خود قرار دهید، سپس دکمه ی آن را لمس کنید. - - - کلید امنیتی YubiKey - "YubiKey" is the product name and should not be translated. - - - افزودن پیوست جدید - - - پیوست ها - - - نمیتوان پرونده را دانلود کرد. - - - دستگاهتان قادر به باز کردن این نوع از فایل نیست. - - - در حال دانلود... - Message shown when downloading a file - - - حجم این پیوست {0} است. آیا مایل به دانلود این فایل به دستگاهتان هستید؟ - The placeholder will show the file size of the attachment. Ex "25 MB" - - - کلید تأیید کننده (TOTP) - - - کد تأیید (TOTP) - Totp code label - - - کلید تأیید کننده اضافه شد. - - - کلید تأیید کننده را نمیتوان خواند. - - - اسکن خودکار رخ می دهد. - - - دوربین خود را بر روی کد QR قرار دهید. - - - اسکن کد QR - - - دوربین - - - عکس ها - - - TOTP کپی شد! - - - کپی TOTP - - - اگر ورود شما دارای یک کلید تأیید کننده میباشد که به آن متصل شده است، هر زمان که بصورت خودکار وارد سایت شوید کد تأیید TOTP به صورت خودکار به کلیپ بورد شما کپی می شود. - - - غیرفعال کردن کپی خودکار TOTP - - - برای استفاده از این ویژگی عضویت پرمیوم لازم است. - - - پیوست اضافه شد - - - پیوست حذف شد - - - انتخاب پرونده - - - فایل - - - هیچ فایلی انتخاب نشده - - - هیچ ضمیمه‌ ای وجود ندارد. - - - فایل امن - - - ویژگی موجود نیست - - - بیشترین حجم فایل ۱۰۰ مگابایت است. - - - تا زمانی که کد رمزنگاری را بروز نکنید نمیتوانید از این قابلیت استفاده کنید. - - - بیشتر بدانید - - - آدرس سرور API - - - محیط های سفارشی - - - برای کاربران پیشرفته. شما می توانید آدرس پایه هر سرویس را مستقل تعیین کنید. - - - آدرس های اینترنتی محیط ذخیره شد. - - - {0} به درستی فرمت نشده. - Validation error when something is not formatted correctly, such as a URL or email address. - - - آدرس سرور شناسایی - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - محیط خود میزبان - - - آدرس اینترنتی پایه فرضی نصب Bitwarden میزبانی شده را مشخص کنید. - - - آدرس اینترنتی سرور - - - آدرس اینترنتی سرور گاوصندوق وب - - - برای مشاهده موارد مربوط به گاوصندوقتان به این اطلاع رسانی ضربه بزنید. - - - فیلدهای سفارشی - - - کپی شماره - - - کپی کد امنیتی - - - شماره - - - کد امنیتی - - - چه نوع موردی میخواهید اضافه کنید؟ - - - کارت - - - مشخصات - - - ورود - - - یادداشت امن - - - نشانی ۱ - - - نشانی ۲ - - - نشانی ۳ - - - آوریل - - - آگوست‌ - - - نام تجاری - - - نام صاحب کارت - - - شهر / شهرک - - - شرکت - - - کشور - - - دسامبر - - - دکتر - - - ماه انقضاء - - - سال انقضاء - - - فوریه - - - نام - - - ژانویه - - - جولای - - - ژوئن - - - نام خانوادگی - - - شماره مجوز - - - مارس - - - مِی - - - نام میانی - - - آقا - - - خانم - - - خانم - - - نوامبر - - - اکتبر - - - شماره پاسپورت - - - تلفن - - - سپتامبر - - - شماره امنیتی اجتماعی - - - ایالت / استان - - - عنوان - - - کد پستی - - - آدرس - - - انقضاء - - - غیرفعال سازی آیکون های وب سایت - - - آیکون های وب سایت یک تصویر مشخص در کنار هر داده ورودی ارائه میدهد. - - - آدرس سرور آیکون ها - - - پر کردن خودکار با Bitwarden - - - گاوصندوق قفل شد - - - برو به گاوصندوق من - - - مجموعه ها - - - در این مجموعه موردی برای نمایش نیست. - - - در این پوشه موردی برای نمایش نیست. - - - سرویس دستیابی به پر کردن خودکار - - - سرویس خودکار Bitwarden با استفاده از چهار چوب پر کردن خودکار اندروید برای کمک به پر کردن ورودی ها، کارت های اعتباری و اطلاعات هویت به سایر برنامه ها در دستگاه شما کمک می کند. - - - از سرویس پر کردن خودکار Bitwarden برای پر کردن ورودها، کارتهای اعتباری، و اطلاعات هویتی درون اپلیکیشن ها استفاده کنید. - - - باز کردن تنظیمات پر کردن خودکار - - - Face ID - What Apple calls their facial recognition reader. - - - از Face ID برای تأیید استفاده شود. - - - از Face ID برای قفل گشایی استفاده شود - - - تأیید Face ID - - - قفل گشایی با Windows Hello - - - تأیید با Windows Hello - - - Windows Hello - - - ما نتوانستیم بصورت خودکار منو تنظیمات پر کردن خودکار اندروید را برای شما باز کنیم. شما میتوانید منوی تنظیمات را بصورت دستی مرور کنید از تنظیمات اندروید > سیستم > زبانها و ورودی > پیشرفته > سرویس پر کردن خودکار. - - - نام فیلد سفارشی - - - بولین - - - مخفی - - - متن - - - فیلد سفارشی جدید - - - چه نوع فیلد سفارشی میخواهید اضافه کنید؟ - - - حذف - - - آدرس اینترنتی جدید - - - آدرس اینترنتی {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - دامنه پایه - - - پیش فرض - - - دقیق - - - میزبان - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - عبارت منظم - A programming term, also known as 'RegEx'. - - - شروع می شود با - - - تشخیص سابقه آدرس اینترنتی - - - تشخیص سابقه - URI match detection for auto-fill. - - - بله، و ذخیره - - - پر کردن خودکار و ذخیره - - - سازماندهی - An entity of multiple related people (ex. a team or business organization). - - - Yubikey خود را در نزدیکی بالای دستگاه نگه دارید. - - - دوباره امتحان کنید - - - برای ادامه، دوباره YubiKey NEO خود را پشت دستگاه نگه دارید. - - - خدمات دسترسی ممکن است مفید باشد برای استفاده در زمانی که برنامه از سرویس پر کردن خودکار استاندارد پشتیبانی نمی کند. - - - گذرواژه بروز رسانی شد - ex. Date this password was updated - - - بروزرسانی شد - ex. Date this item was updated - - - پرکردن خودکار فعال شد! - - - شما بایستی به برنامه اصلی Bitwarden وارد شوید تا بتوانید از پرکردن خودکار استفاده کنید. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - پیشهاز ما این است که اگر از اپ های پرکردن خودکار دیگری استفاده میکنید آنها را غیرفعال کنید اگر برنامه ای برای استفاده از آنها ندارید. - - - دسترسی به گاوصندوقتان بصورت مستقیم از طرق کیبوردتان برای سریعتر پرکردن خودکار رمزهای عبور. - - - برای فعال کردن پر کردن خودکار کلمه عبور بر روی دستگاهتان، این دستورالعمل ها را دنبال کنید: - - - ۱. برو به اپ "تنظیمات" ios - - - ۲. بزن روی "کلمات عبور و حسابها" - - - ۳. بزن روی "پرکردن خودکار کلمات عبور" - - - ۴. روشن کردن پرکردن خودکار - - - ۵. انتخاب "Bitwarden" - - - پرکردن خودکار کلمه عبور - - - ساده ترین روش برای افزودن اطلاعات ورود به گاوصندوق از طریق افزونه پرکردن خودکار رمز عبور Bitwarden است. برای اطلاع بیشتر از چگونگی استفاده از پرکردن خودکار رمز عبور Bitwarden مرورکردن صفحه "ابزار" است. - - diff --git a/src/App/Resources/AppResources.fi.Designer.cs b/src/App/Resources/AppResources.fi.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.fi.resx b/src/App/Resources/AppResources.fi.resx deleted file mode 100644 index 247d2663f..000000000 --- a/src/App/Resources/AppResources.fi.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Tietoja - - - Lisää - Add/create a new entity (verb). - - - Lisää kansio - - - Lisää kohde - The title for the add item page. - - - Tapahtui virhe. - Alert title when something goes wrong. - - - Takaisin - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Peruuta - Cancel an operation. - - - Kopioi - Copy some value to your clipboard. - - - Kopioi salasana - The button text that allows a user to copy the login's password to their clipboard. - - - Kopioi käyttäjänimi - The button text that allows a user to copy the login's username to their clipboard. - - - Kiitokset - Title for page that we use to give credit to resources that we use. - - - Poista - Delete an entity (verb). - - - Poistetaan… - Message shown when interacting with the server - - - Haluatko varmasti poistaa? Tätä toimintoa ei voi peruuttaa. - Confirmation alert message when deleteing something. - - - Muokkaa - - - Muokkaa kansiota - - - Sähköposti - Short label for an email address. - - - Sähköpostiosoite - Full label for a email address. - - - Lähetä sähköpostia - - - Lähetä meille sähköpostia saadaksesi apua tai jättääksesi palautetta. - - - Syötä PIN-koodisi. - - - Suosikit - Title for your favorite items in the vault. - - - Jätä virheilmoitus - - - Jätä ilmoitus GitHub-pakettivarastoomme. - - - Tunnistaudu sormenjäljelläsi. - - - Kansio - Label for a folder. - - - Uusi kansio luotu. - - - Kansio poistettu. - - - Ei kansiota - Items that have no folder specified go in this special "catch-all" folder. - - - Kansiot - - - Kansio päivitetty. - - - Mene sivustolle - The button text that allows user to launch the website to their web browser. - - - Tuki ja palaute - - - Piilota - Hide a secret value that is currently shown (password). - - - Yhdistä internetiin jatkaaksesi. - Description message for the alert when internet connection is required to continue. - - - Internet-yhteys vaaditaan - Title for the alert when internet connection is required to continue. - - - Virheellinen pääsalasana. Yritä uudelleen. - - - Virheellinen PIN. Yritä uudelleen. - - - Avaa - The button text that allows user to launch the website to their web browser. - - - Kirjaudu sisään - The login button text (verb). - - - Käyttäjätunnus - Title for login page. (noun) - - - Kirjaudu ulos - The log out button text (verb). - - - Haluatko varmasti kirjautua ulos? - - - Pääsalasana - Label for a master password. - - - Lisää - Text to define that there are more options things to see. - - - Minun holvini - The title for the vault page. - - - Nimi - Label for an entity name. - - - En - - - Merkinnät - Label for notes. - - - Ok - Acknowledgement. - - - Salasana - Label for a password. - - - Tallenna - Button text for a save operation (verb). - - - Tallennetaan… - Message shown when interacting with the server - - - Asetukset - The title for the settings page. - - - Näytä - Reveal a hidden value (password). - - - Kohde poistettu. - Confirmation message after successfully deleting a login. - - - Jatka - - - Synkronointi - The title for the sync page. - - - Kiitokset - - - Työkalut - The title for the tools page. - - - URI - Label for a uri/url. - - - Avaa holvi sormenjäljellä - - - Käyttäjänimi - Label for a username. - - - Kenttä ”{0}” vaaditaan. - Validation message for when a form field is left blank and is required to be entered. - - - {0} kopioitiin. - Confirmation message after suceessfully copying a value to the clipboard. - - - Varmista sormenjälki - - - Varmista pääsalasana - - - Varmista PIN - - - Versio - - - Näytä - - - Käy sivustollamme - - - Käy sivustollamme, jossa voit saada apua Bitwardenin käyttämisessä, lukea palvelun uutisia tai lähettää meille sähköpostia. - - - SIvusto - Label for a website. - - - Kyllä - - - Tili - - - Uusi tilisi on luotu! Voit nyt kirjautua sisään. - - - Lisää kohde - - - Sovelluslaajennus - - - Käytä Bitwardenin esteettömyyspalvelua kirjautumiskenttien automaattiseen täyttämiseen sovelluksissa ja verkossa. - - - Automaattinen täyttö ‑palvelu - - - Vältä epäselviä merkkejä - - - Bitwarden-sovelluslaajennus - - - Helpoin tapa lisätä uusia kirjautumistietoja holviisi on käyttää bitwardenin sovelluslaajennusta. Lue lisää sovelluslaajennuksesta "Työkalut"-kohdassa. - - - Käytä bitwardenia Safarissa ja muissa sovelluksissa kirjautumistietojen automaattiseen täydennykseen. - - - Bitwardenin automaattinen täyttö ‑palvelu - - - Use the bitwarden accessibility service to auto-fill your logins. - - - Vaihda sähköpostiosoite - - - Voit vaihtaa sähköpostiosoitteesi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt? - - - Vaihda pääsalasana - - - Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt? - - - Sulje - - - Tulossa pian! - - - Jatka - - - Kopioitu! - - - Salasana kopioitu! - - - Käyttäjänimi kopioitu! - - - Luo tili - - - Luodaan tiliä… - Message shown when interacting with the server - - - Muokkaa kohdetta - - - Käytä automaattista synkronointia - - - Syötä tilisi sähköpostiosoite saadaksesi pääsalasanasi vihjeen. - - - Käytä sovelluslaajennusta - - - Melkein valmis! - - - Käytä sovelluslaajennusta - - - Safarissa löydät bitwardenin käyttämällä jakokuvaketta (vihje: vieritä oikealle valikon alarivillä). - Safari is the name of apple's web browser - - - Välitön pääsy salasanoihisi! - - - Olet valmis kirjautumaan sisään! - - - Katso tuetut sovellukset - - - Kirjautumistietosi ovat nyt helposti saatavilla Safarissa, Chromessa ja muissa tuetuissa sovelluksissa. - - - Safarissa ja Chromessa löydät bitwardenin käyttämällä jakokuvaketta (vihje: vieritä oikealle jakovalikon alarivillä). - - - Paina bitwarden-kuvaketta valikossa käynnistääksesi laajennuksen. - - - Ottaaksesi bitwardenin käyttöön Safarissa ja muissa sovelluksissa paina "lisää"-kuvaketta valikon alarivillä. - - - Suosikki - - - Sormenjälki - - - Luo salasana - - - Pyydä salasanavihjettäsi - - - Tuo kohteita - - - Voit tuoda kerralla kaikki kohteesi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt? - - - Tuo kerralla kohteitasi muista salasananhallintapalveluista. - - - Viimeisin synkronointi: - - - Pituus - - - Lukitse - - - 15 minuuttia - - - 1 tunti - - - 1 minuutti - - - 4 tuntia - - - Välittömästi - - - Lukitseminen - - - Kirjaudutaan sisään… - Message shown when interacting with the server - - - Kirjaudu sisään tai luo tili päästäksesi suojattuun holviisi. - - - Hallinta - - - Salasanan varmistus ei täsmää. - - - Pääsalasanalla pääset käsiksi salasanaholviisi. On hyvin tärkeää, että muistat pääsalasanasi. Pääsalasanaa ei voida palauttaa mitenkään, jos satut unohtamaan sen. - - - Pääsalasanavihje (valinnainen) - - - Salasanavihje voi auttaa sinua muistamaan pääsalasanasi, jos satut unohtamaan sen. - - - Pääsalasanan on oltava vähintään 8 merkkiä pitkä. - - - Numeroja vähintään - Minimum numeric characters for password generator settings - - - Erikoismerkkejä vähintään - Minimum special characters for password generator settings - - - Lisää asetuksia - - - Sinun on kirjauduttava bitwardenin pääsovellukseen ennen kuin voit käyttää laajennusta. - - - Ei koskaan - - - Uusi kohde luotu. - - - Holvissasi ei ole suosikkeja. - - - Holvissasi ei ole kohteita. - - - Holvissasi ei ole kohteita tälle sivustolle. Napauta lisätäksesi kohteen. - - - Tälle käyttäjätunnukselle ei ole määritetty käyttäjänimeä tai salasanaa. - - - Ymmärrän! - Confirmation, like "Ok, I understand it" - - - Oletusvalinnat määritetään pääsovelluksen salasanageneraattorista. - - - Asetukset - - - Muut - - - Salasana luotu. - - - Salasanageneraattori - - - Salasanavihje - - - Lähetimme sinulle sähköpostilla pääsalasanasi vihjeen. - - - Haluatko varmasti korvata nykyisen salasanan? - - - bitwarden pitää holvisi automaattisesti synkronoituna käyttämällä ilmoituspalvelua. Parhaan kokemuksen takaamiseksi kannattaa ottaa ilmoitukset käyttöön seuraavassa ruudussa. - Push notifications for apple products - - - Arvostele sovellus - - - Harkitsethan auttamistamme jättämällä positiivisen arvostelun! - - - App Storen arvostelut nollautuvat Bitwardenin jokaisen päivityksen yhteydessä. Autathan meitä jättämällä hyvän arvostelun! - - - Luo uusi salasana - - - Syötä pääsalasana uudelleen - - - Hae holvista - - - Turvallisuus - - - Seuraa kehityksen edistymistä - - - Valitse - - - Aseta PIN - - - Syötä 4-numeroinen PIN-koodi, jonka avulla voit avata sovelluksen. - - - Kohteen tiedot - - - Kohde päivitetty. - - - Käsitellään… - Message shown when interacting with the server - - - Synkronoidaan… - Message shown when interacting with the server - - - Synkronointi valmis. - - - Synkronointi epäonnistui. - - - Synkronoi holvi nyt - - - Touch ID - What Apple calls their fingerprint reader. - - - Kaksivaiheinen kirjautuminen - - - Kaksivaiheinen kirjautuminen tekee tilistäsi turvallisemman vaatimalla koodin tunnistautumispalvelusta joka kirjautumiskerralla. Voit ottaa kaksivaiheisen tunnistautumisen käyttöön bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt? - - - Avaa sormenjäljellä - - - Avaa PIN-koodilla - - - Vahvistetaan - Message shown when interacting with the server - - - Vahvistuskoodi - - - Näytä kohde - - - Bitwardenin verkkoholvi - - - Hallitse kohteitasi millä tahansa selaimella Bitwardenin verkkoholvissa. - - - Etkö pysty käyttämään todennussovellustasi? - - - Kohteet - Screen title - - - Laajennus aktivoitu! - - - Kuvakkeet - - - Käännökset - - - Kohteet palvelulle {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Holvissasi ei ole kohteita palvelulle {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Kun näet ilmoituksen bitwardenin automaattisesta täydennyksestä, voit napauttaa sitä käynnistääksesi palvelun. - - - Napauta tätä ilmoitusta täydentääksesi kohteen automaattisesti. - - - Avaa esteettömyysasetukset - - - 1. Valitse esteettömyysasetuksissa Palvelut-otsikon alta ”Bitwarden”. - - - 2. Laita asetus päälle ja paina OK-painiketta hyväksyäksesi. - - - Pois käytöstä - - - Käytössä - - - Tila - - - Beeta - - - Helpoin tapa lisätä uusia käyttäjätunnuksia holviisi on käyttää Bitwardenin automaattinen täyttö ‑palvelua. Lue lisää palvelusta ”Työkalut”-kohdassa. - - - Täytä automaattisesti - - - Haluatko täyttää tällä kohteella vai näyttää sen tiedot? - - - Haluatko varmasti täyttää tällä kohteella? Se ei täsmää täysin osoitteeseen "{0}". - - - Täsmäävät kohteet - - - Mahdollisesti täsmäävät kohteet - - - Hae - - - Etsitään täytettäviä kohteita osoitteelle "{0}". - - - Jaa holvisi - - - Luo organisaatio jakaaksesi kirjautumistiedot turvallisesti muiden käyttäjien kanssa. - - - Lue, kun salasanakenttä on aktiivinen - - - Skannaa kentät ja näytä ilmoitus automaattisesta täydentämisestä vain silloin, kun valitset salasanakentän. Tämä asetus saattaa vähentää virrankulutusta. - - - Pysyvä ilmoitus - - - Näytä aina ilmoitus automaattisesta täyttämisestä. Kentät skannataan vasta, kun automaattista täydennystä on yritetty. Tämä asetus saattaa vähentää virrankulutusta. - - - Skannaa aina - - - Skannaa ruutua kenttien varalta ja näytä ilmoitus automaattisesta täydentämisestä, kun salasanakenttä löytyy. Tämä on oletusasetus. - - - Sovelluksen ”{0}” avaaminen ei onnistunut. - Message shown when trying to launch an app that does not exist on the user's device. - - - Todennussovellus - For 2FA - - - Syötä 6-numeroinen vahvistuskoodi todennussovelluksestasi. - For 2FA - - - Syötä 6-numeroinen vahvistuskoodi, joka lähetettiin osoitteeseen {0}. - For 2FA - - - Kirjautuminen epäonnistui - For 2FA whenever there are no available providers on this device. - - - Tällä tilillä on kaksivaiheinen kirjautuminen käytössä, mutta tämä laite ei tue yhtäkään määritetyistä kaksivaihekirjautumisen menetelmistä. Käytä tuettua laitetta ja/tai ota käyttöön paremmin tuettu todennusmenetelmä (kuten todennussovellus). - - - Palautuskoodi - For 2FA - - - Muista minut - Remember my two-step login - - - Lähetä vahvistuskoodisähköposti uudestaan - For 2FA - - - Kaksivaiheisen kirjautumisen asetukset - - - Käytä toista menetelmää kaksivaiheiseen kirjautumiseen - - - Vahvistussähköpostin lähetys epäonnistui. Yritä uudelleen. - For 2FA - - - Vahvistussähköposti lähetetty. - For 2FA - - - Hold your YubiKey NEO against the back of the device to continue. - - - YubiKey-todennuslaite - "YubiKey" is the product name and should not be translated. - - - Lisää uusi liite - - - Liitteet - - - Tiedoston lataaminen epäonnistui. - - - Laitteesi ei voi avata tämän tyyppistä tiedostoa. - - - Ladataan… - Message shown when downloading a file - - - Tämän liitteen koko on {0}. Haluatko varmasti ladata sen laitteellesi? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Todennusavain (TOTP) - - - Vahvistuskoodi (TOTP) - Totp code label - - - Todennusavain lisätty. - - - Todennusavaimen luku epäonnistui. - - - Koodi luetaan automaattisesti. - - - Kohdista kamerasi QR-koodiin. - - - Lue QR-koodi - - - Kamera - - - Kuvat - - - TOTP kopioitu! - - - Kopioi TOTP - - - Jos käyttäjätunnukseesi on liitetty todennusavain, TOTP-vahvistuskoodi kopioidaan automaattisesti leikepöydällesi tehdessäsi kohteella automaattisen täytön. - - - Älä kopioi TOTP-koodia automaattisesti - - - Tarvitset premium-jäsenyyden tämän toiminnon käyttämiseen. - - - Liite lisätty - - - Liite poistettu - - - Valitse tiedosto - - - Tiedosto - - - Ei valittua tiedostoa - - - Ei liitteitä. - - - Tiedoston lähde - - - Toiminto ei ole käytettävissä - - - Tiedoston enimmäiskoko on 100 MB. - - - Et voi käyttää tätä toimintoa ennen kuin päivität salausavaimesi. - - - Lue lisää - - - API-palvelimen URL - - - Mukautettu ohjelmistoympäristö - - - Edistyneille käyttäjille. Voit syöttää jokaisen palvelun kanta-URL:n erikseen. - - - Ohjelmistoympäristön URL:t tallennettu. - - - {0} on väärässä muodossa. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identiteettipalvelimen URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Itse ylläpidetty ohjelmistoympäristö - - - Syötä omalla palvelimella ylläpitämäsi Bitwarden-asennuksen kanta-URL. - - - Palvelimen URL - - - Verkkoholvipalvelimen URL - - - Napauta tätä ilmoitusta näyttääksesi holvisi kohteet. - - - Lisäkentät - - - Kopioi numero - - - Kopioi turvakoodi - - - Numero - - - Turvakoodi - - - Minkä kohteen haluat lisätä? - - - Kortti - - - Henkilöllisyys - - - Käyttäjätunnus - - - Salattu muistiinpano - - - Osoite 1 - - - Osoite 2 - - - Osoite 3 - - - Huhtikuu - - - Elokuu - - - Merkki - - - Kortinhaltijan nimi - - - Paikkakunta - - - Yhtiö - - - Maa - - - Joulukuu - - - Tri - - - Erääntymiskuukausi - - - Erääntymisvuosi - - - Helmikuu - - - Etunimi - - - Tammikuu - - - Heinäkuu - - - Kesäkuu - - - Sukunimi - - - Rekisterinumero - - - Maaliskuu - - - Toukokuu - - - Välinimi - - - Hra - - - Rva - - - Nti - - - Marraskuu - - - Lokakuu - - - Passin numero - - - Puhelinnumero - - - Syyskuu - - - Henkilötunnus - - - Osavaltio/maakunta - - - Titteli - - - Postinumero - - - Osoite - - - Erääntymisaika - - - Älä näytä sivustokuvakkeita - - - Holvisi käyttäjätunnuslistassa näytettävät sivustokuvakkeet helpottavat kohteiden tunnistamista. - - - Kuvakepalvelimen URL - - - Täytä automaattisesti Bitwardenilla - - - Holvi on lukittu - - - Mene holviini - - - Valikoimat - - - Tässä valikoimassa ei ole kohteita. - - - Tässä kansiossa ei ole kohteita. - - - Automaattisen täytön esteettömyyspalvelu - - - The bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. - - - Käytä Bitwardenin esteettömyyspalvelua täyttääksesi automaattisesti käyttäjätunnukset, kortti- ja henkilöllisyystiedot muihin sovelluksiin. - - - Avaa automaattisen täytön asetukset - - - Face ID - What Apple calls their facial recognition reader. - - - Käytä Face ID:tä tunnistautuaksesi. - - - Avaa holvi Face ID:llä - - - Tunnistaudu Face ID:llä - - - Avaa holvi Windows Hellolla - - - Tunnistaudu Windows Hellolla - - - Windows Hello - - - Androidin automaattisen täytön asetusten avaaminen epäonnistui. Voit mennä sinne manuaalisesti näin: Asetukset > Järjestelmä > Kielet ja syöttötapa > Lisäasetukset > Automaattinen täyttö ‑palvelu. - - - Lisäkentän nimi - - - Valintaruutu - - - Piilotettu teksti - - - Teksti - - - Uusi lisäkenttä - - - Minkä tyyppisen lisäkentän haluat lisätä? - - - Poista - - - Uusi URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Pääverkkotunnus - - - Oletus - - - Täsmälleen… - - - Osoite - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Säännöllinen lauseke - A programming term, also known as 'RegEx'. - - - Alkaa… - - - URIn tunnistustapa - - - Tunnistustapa - URI match detection for auto-fill. - - - Yes, and Save - - - Auto-fill and save - - - Organisaatio - An entity of multiple related people (ex. a team or business organization). - - - Pidä Yubikeytasi lähellä laitteen yläosaa. - - - Yritä uudelleen - - - To continue, hold your YubiKey NEO against the back of the device. - - - The accessibility service may be helpful to use when apps do not support the standard auto-fill service. - - - Salasana päivitetty - ex. Date this password was updated - - - Päivitetty - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.fr.Designer.cs b/src/App/Resources/AppResources.fr.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.fr.resx b/src/App/Resources/AppResources.fr.resx deleted file mode 100644 index a65c780f8..000000000 --- a/src/App/Resources/AppResources.fr.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - À propos - - - Ajouter - Add/create a new entity (verb). - - - Ajouter un dossier - - - Ajouter un élément - The title for the add item page. - - - Une erreur est survenue. - Alert title when something goes wrong. - - - Retour - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Annuler - Cancel an operation. - - - Copier - Copy some value to your clipboard. - - - Copier mot de passe - The button text that allows a user to copy the login's password to their clipboard. - - - Copier nom d'utilisateur - The button text that allows a user to copy the login's username to their clipboard. - - - Remerciements - Title for page that we use to give credit to resources that we use. - - - Supprimer - Delete an entity (verb). - - - Suppression... - Message shown when interacting with the server - - - Êtes-vous sûr(e) de vouloir supprimer ? Ne peut être annulé. - Confirmation alert message when deleteing something. - - - Modifier - - - Modifier dossier - - - E-mail - Short label for an email address. - - - Adresse e-mail - Full label for a email address. - - - Nous contacter - - - Contactez-nous directement pour obtenir de l'aide ou pour nous faire part d'un commentaire. - - - Saisissez votre code PIN. - - - Favoris - Title for your favorite items in the vault. - - - Envoyer un rapport de bogue - - - Ouvrir un ticket dans notre dépôt Github. - - - Utiliser votre empreinte pour vous authentifier. - - - Dossier - Label for a folder. - - - Nouveau dossier créé. - - - Dossier supprimé. - - - Pas de dossier - Items that have no folder specified go in this special "catch-all" folder. - - - Dossiers - - - Dossier mis à jour. - - - Visiter le site web - The button text that allows user to launch the website to their web browser. - - - Aide et retour - - - Masquer - Hide a secret value that is currently shown (password). - - - Veuillez vous connecter à Internet avant de poursuivre. - Description message for the alert when internet connection is required to continue. - - - Connexion Internet requise - Title for the alert when internet connection is required to continue. - - - Mot de passe maître invalide. Veuillez réessayer. - - - PIN invalide. Veuillez réessayer. - - - Démarrer - The button text that allows user to launch the website to their web browser. - - - S'identifier - The login button text (verb). - - - Nom d'utilisateur - Title for login page. (noun) - - - Déconnexion - The log out button text (verb). - - - Êtes-vous sûr(e) de vouloir vous déconnecter ? - - - Mot de passe maître - Label for a master password. - - - Plus - Text to define that there are more options things to see. - - - Mon coffre - The title for the vault page. - - - Nom - Label for an entity name. - - - Non - - - Notes - Label for notes. - - - Ok - Acknowledgement. - - - Mot de passe - Label for a password. - - - Enregistrer - Button text for a save operation (verb). - - - Enregistrement... - Message shown when interacting with the server - - - Paramètres - The title for the settings page. - - - Afficher - Reveal a hidden value (password). - - - L'élément a été supprimé. - Confirmation message after successfully deleting a login. - - - Soumettre - - - Synchronisation - The title for the sync page. - - - Merci - - - Outils - The title for the tools page. - - - URI - Label for a uri/url. - - - Utiliser votre empreinte pour déverrouiller - - - Nom d'utilisateur - Label for a username. - - - Le champ {0} est requis. - Validation message for when a form field is left blank and is required to be entered. - - - {0} a été copié. - Confirmation message after suceessfully copying a value to the clipboard. - - - S'authentifier avec l'empreinte - - - S'authentifier avec le mot de passe maître - - - S'authentifier avec un code PIN - - - Version - - - Voir - - - Visiter notre site web - - - Visitez notre site web pour obtenir de l'aide, lire les actualités, nous écrire et/ou apprendre à mieux utiliser Bitwarden. - - - Site web - Label for a website. - - - Oui - - - Compte - - - Votre nouveau compte a été créé ! Vous pouvez maintenant vous identifier. - - - Ajouter un élément - - - Extension de l'application - - - Utiliser le service d'accessibilité de Bitwarden pour remplir automatiquement vos identifiants sur les applis et le web. - - - Service de remplissage automatique - - - Évitez les caractères ambigus - - - Extension de l'application Bitwarden - - - Le meilleur moyen d'ajouter de nouveaux sites à votre coffre est d'utiliser l'extension de l'application Bitwarden. Parcourez le menu "Outils" pour en apprendre davantage. - - - Utilisez Bitwarden dans Safari et d'autres applications pour le remplissage automatique de vos identifiants. - - - Service de saisie automatique Bitwarden - - - Utilisez le service d'accessibilité de Bitwarden pour remplir automatiquement vos identifiants. - - - Changer d'e-mail - - - Vous pouvez changer votre adresse e-mail depuis le coffre web sur bitwarden.com. Voulez-vous visiter le site web maintenant ? - - - Changer le mot de passe maître - - - Vous pouvez changer votre mot de passe maître depuis le coffre web sur bitwarden.com. Voulez-vous visiter le site web maintenant ? - - - Fermer - - - Prochainement ! - - - Continuer - - - Copié! - - - Mot de passe copié ! - - - Nom d'utilisateur copié ! - - - Créer un compte - - - Création du compte... - Message shown when interacting with the server - - - Modifier l'élément - - - Activer la synchronisation automatique - - - Saisissez l'adresse e-mail de votre compte pour recevoir l'indice de votre mot de passe maître. - - - Activez à nouveau l'extension de l'application - - - Presque terminé ! - - - Activez l'extension de l'application - - - Sur Safari, trouvez Bitwarden en utilisant l'icône de partage (aide : faire défiler sur la droite sur la ligne du bas du menu). - Safari is the name of apple's web browser - - - Obtenir un accès instantané à vos mots de passe ! - - - Vous êtes prêt à vous identifier ! - - - Voir les applications supportées - - - Vos identifiants sont désormais facilement accessibles depuis Safari, Chrome, et d'autres applications supportées. - - - Sur Safari et Chrome, trouvez Bitwarden en utilisant l'icône de partage (aide : faire défiler sur la droite sur la ligne du bas du menu). - - - Appuyez sur l'icône Bitwarden dans le menu pour lancer l'extension. - - - Pour activer Bitwarden sur Safari et d'autres applications, appuyez sur l'icône "plus" sur la ligne du bas du menu. - - - Favori - - - Empreinte - - - Générer un mot de passe - - - Obtenir votre indice de mot de passe maître - - - Importer des éléments - - - Vous pouvez importer en masse vos identifiants depuis le coffre web sur bitwarden.com. Souhaitez-vous visiter le site maintenant ? - - - Importer rapidement en masse vos identifiants depuis d'autres applications de gestion de mots de passe. - - - Dernière synchronisation : - - - Longueur - - - Verrouiller - - - 15 minutes - - - 1 heure - - - 1 minute - - - 4 heures - - - Immédiatement - - - Options de verrouillage - - - Identification... - Message shown when interacting with the server - - - Identifiez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé. - - - Gérer - - - La confirmation du mot de passe est erronée. - - - Le mot de passe maître correspond au mot de passe utilisé pour accéder à votre coffre. Il est très important de ne pas l'oublier. Il n'existe aucun moyen de le retrouver en cas d'oubli. - - - Indice de mot de passe maître (facultatif) - - - Un indice de mot de passe maître peut vous aider à vous rappeler de votre mot de passe en cas d'oubli. - - - Le mot de passe maître doit faire plus de 8 caractères. - - - Nombre minimum de chiffres - Minimum numeric characters for password generator settings - - - Nombre minimum de caractères spéciaux - Minimum special characters for password generator settings - - - Plus de paramètres - - - Vous devez vous identifier sur l'application principale Bitwarden avant d'utiliser l'extension. - - - Jamais - - - Nouvel élément créé. - - - Aucun favori dans votre coffre. - - - Il n’y a aucun élément dans votre coffre. - - - Il n’y a aucun élément dans votre coffre pour ce site. Appuyer pour en ajouter un. - - - Cet identifiant n'a ni nom d'utilisateur ni mot de passe de configuré. - - - Ok, compris ! - Confirmation, like "Ok, I understand it" - - - Les options par défaut sont définies depuis l'outil de génération de mot de passe de l'application principale Bitwarden. - - - Options - - - Autres - - - Mot de passe généré. - - - Générateur de mot de passe - - - Indice du mot de passe - - - Nous nous avons envoyé un e-mail avec l'indice de votre mot de passe. - - - Êtes-vous sûr(e) de vouloir écraser le mot de passe existant ? - - - Bitwarden conserve automatiquement votre coffre synchronisé en utilisant des notifications push. Pour la meilleure expérience possible, veuillez choisir "Ok" sur la boîte de dialogue suivante (activation des notifications push). - Push notifications for apple products - - - Noter l'application - - - Merci de nous aider en rédigeant un commentaire positif ! - - - Les notes des magasins d'applications sont réinitialisées à chaque nouvelle version de Bitwarden. Merci de nous aider en rédigeant un commentaire positif ! - - - Regénérer un mot de passe - - - Saisir à nouveau le mot de passe maître - - - Recherche dans le coffre - - - Sécurité - - - Voir le suivi du développement - - - Sélectionner - - - Définir PIN - - - Saisissez un code PIN à 4 chiffres pour déverrouiller l'application. - - - Informations sur l'élément - - - Élément mis à jour. - - - Soumission... - Message shown when interacting with the server - - - Synchronisation... - Message shown when interacting with the server - - - Synchronisation terminée. - - - Échec de la synchronisation. - - - Synchroniser le coffre maintenant - - - Touch ID - What Apple calls their fingerprint reader. - - - Identification à double facteurs - - - L'authentification à double facteurs rend votre compte plus sécurisé en vous demandant la saisie d'un code de sécurité à chaque identification depuis l'application d'authentification. L'identification à double facteurs peut être activée dans le coffre web sur bitwarden.com. Voulez-vous visiter le site web maintenant ? - - - Déverrouiller avec {0} - - - Déverrouiller avec un code PIN - - - Validation - Message shown when interacting with the server - - - Code de vérification - - - Afficher l'élément - - - Coffre web Bitwarden - - - Gérer vos identifiants depuis n'importe quel navigateur avec le coffre web Bitwarden. - - - Application d'authentification perdue ? - - - Éléments - Screen title - - - Extension activée ! - - - Icônes - - - Traductions - - - Éléments pour {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Il n’y a aucun élément dans votre coffre pour {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Lorsque vous voyez une notification de remplissage automatique de la part de Bitwarden, vous pouvez la toucher pour démarrer le service de remplissage automatique. - - - Touchez cette notification pour remplir automatiquement vos informations de connexion à partir de votre coffre. - - - Ouvrir les paramètres d'accessibilité - - - 1. Dans les paramètres d'accessibilité d'Android, sélectionnez « Bitwarden » dans la section « Services ». - - - 2. Activez l'interrupteur et appuyez sur OK pour accepter. - - - Désactivé - - - Activé - - - État - - - Bêta - - - L'option de remplissage automatique est la façon la plus simple d'ajouter de nouveaux identifiants à votre coffre. Apprenez-en plus sur l'option de remplissage automatique de Bitwarden en vous rendant dans le menu « Outils ». - - - Remplissage automatique - - - Voulez-vous remplir automatiquement ou afficher cet élément ? - - - Êtes-vous sûr(e) de vouloir remplir automatiquement cet élément ? Il ne correspond pas complètement à « {0} ». - - - Aucun élément correspondant - - - Éléments pouvant correspondre - - - Rechercher - - - Vous recherchez un élément pour remplir automatiquement « {0} ». - - - Partager votre coffre - - - Créez une organisation pour partager de manière sécurisée vos identifiants avec d'autres utilisateurs. - - - Scanner quand le champ Mot de passe est sélectionné - - - Si champ "Mot de passe" sélectionné : déclenchement d'un scan et notification d’auto-complétion. Ceci peut aider à économiser de la batterie. - - - Notification Persistante - - - Notification d'auto-complétion persistante et scanner uniquement après avoir appuyé sur la notification. Ceci peut aider à économiser de la batterie. - - - Toujours scanner - - - Scanner en permanence l'écran à la recherche de champs et uniquement proposer la notification d'auto-complétion si des champs "Mot de passe" sont trouvés. Ceci est le paramètre par défaut. - - - Impossible d'ouvrir l'application "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Application d'authentification - For 2FA - - - Saisissez le code de vérification à 6 chiffres depuis votre application d'authentification. - For 2FA - - - Saisissez le code de vérification à 6 chiffres qui vous a été envoyé par e-mail à {0}. - For 2FA - - - Identifiant non disponible - For 2FA whenever there are no available providers on this device. - - - Ce compte utilise l'authentification à double facteurs, mais aucun des services d'authentification à double facteurs n'est supporté sur cet appareil. Veuillez utiliser un appareil compatible et/ou ajouter des services supplémentaires qui sont mieux supportés sur les appareils (comme une application d'authentification). - - - Code de récupération - For 2FA - - - Rester connecté - Remember my two-step login - - - Envoyer à nouveau l'e-mail du code de vérification - For 2FA - - - Options d'identification à double facteurs - - - Utiliser une autre méthode d'identification à double facteurs - - - Impossible d'envoyer l'e-mail de vérification. Essayez à nouveau. - For 2FA - - - L'e-mail de vérification a été envoyé. - For 2FA - - - Pour continuer, maintenez votre YubiKey NEO à l'arrière de l'appareil ou insérez votre Yubikey dans le port USB de l'appareil puis appuyez sur son bouton. - - - Clé de sécurité YubiKey NEO - "YubiKey" is the product name and should not be translated. - - - Ajouter une nouvelle pièce jointe - - - Pièces jointes - - - Impossible de télécharger le fichier. - - - Votre appareil ne peut pas ouvrir ce type de fichier. - - - Téléchargement... - Message shown when downloading a file - - - Cette pièce jointe est d'une taille de {0}. Êtes-sûr(e) de vouloir la télécharger sur votre appareil ? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Clé d'authentification (TOTP) - - - Code de vérification (TOTP) - Totp code label - - - Clé d'authentification ajoutée. - - - Impossible de lire la clé d'authentification. - - - Le scannage se fera automatiquement. - - - Pointez votre appareil photo sur le QR code. - - - Scanner le QR code - - - Appareil photo - - - Photos - - - TOTP copié ! - - - Copier le TOTP - - - Si une clé d'authentification est rattachée à votre identifiant, alors le code de vérification TOTP est automatiquement copié dans le presse-papiers lorsque vous renseignez l'identifiant. - - - Désactiver la copie automatique du TOTP - - - Une adhésion premium est requises pour utiliser cette fonctionnalité. - - - Pièce jointe ajoutée - - - Pièce jointe supprimée - - - Choisir un fichier - - - Fichier - - - Aucun fichier choisi - - - Il n'y a pas de pièces jointes. - - - Source du fichier - - - Fonctionnalité non disponible - - - La taille maximale du fichier est de 100 Mo. - - - Vous ne pouvez pas utiliser cette fonctionnalité tant que vous ne mettez pas à jour votre clé de chiffrement. - - - En savoir plus - - - URL du serveur de l'API - - - Environnement personnalisé - - - Pour les utilisateurs avancés. Vous pouvez spécifier l'URL de base de chaque service indépendamment. - - - Les URLs d'environnement ont été enregistrées. - - - {0} n’est pas correctement formaté. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL du serveur d'identification - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Environnement auto-hébergé - - - Spécifiez l'URL de base de votre installation Bitwarden auto-hébergée. - - - URL du serveur - - - URL du serveur du coffre web - - - Touchez cette notification pour voir les éléments de votre coffre. - - - Champs personnalisés - - - Copier le numéro - - - Copier le code de sécurité - - - Numéro - - - Code de sécurité - - - Quel type d'élément voulez-vous ajouter ? - - - Carte de paiement - - - Identité - - - Identifiant - - - Note sécurisée - - - Adresse 1 - - - Adresse 2 - - - Adresse 3 - - - Avril - - - Août - - - Réseau de paiement - - - Nom du titulaire de la carte - - - Ville - - - Entreprise - - - Pays - - - Décembre - - - Dr - - - Mois d'expiration de la carte - - - Année d'expiration de la carte - - - Février - - - Prénom - - - Janvier - - - Juillet - - - Juin - - - Nom de famille - - - Numéro de permis - - - Mars - - - Mai - - - Deuxième prénom - - - M. - - - Mme - - - Mlle - - - Novembre - - - Octobre - - - Numéro de passeport - - - Téléphone - - - Septembre - - - Numéro de sécurité sociale - - - Dept. / Province - - - Titre - - - Code postal - - - Adresse - - - Date d'expiration - - - Désactiver les icônes des sites web - - - Les icônes des sites web permettent d'avoir une icône reconnaissable à côté de chaque identifiant dans votre coffre. - - - URL du serveur d’icônes - - - Saisie automatique avec Bitwarden - - - Le coffre est verrouillé - - - Aller dans mon coffre - - - Collections - - - Aucun élément dans cette collection. - - - Aucun élément dans ce dossier. - - - Service d'accessibilité pour la saisie automatique - - - Le service de saisie automatique de Bitwarden utilise l'outil de saisie automatique d'Android pour aider à saisir les identifiants, les cartes de crédit et les informations d'identité dans d'autres applis sur votre appareil. - - - Utilisez le service d'accessibilité de Bitwarden pour la saisie automatique de vos identifiants. - - - Ouvrir les paramètres de saisie automatique - - - Face ID - What Apple calls their facial recognition reader. - - - Utiliser Face ID pour vous authentifier. - - - Utiliser Face ID pour déverrouiller - - - S'authentifier avec Face ID - - - Déverouiller avec Windows Hello - - - S'authentifier avec Windows Hello - - - Windows Hello - - - Nous n'avons pas pu ouvrir automatiquement le menu des paramètres de saisie automatique d'Android. Vous pouvez manuellement naviguer vers le menu des paramètres de saisie automatique à partir des paramètres Android > Système > Langues et saisie > Paramètres avancés > Service de saisie automatique. - - - Nom du champ personnalisé - - - Booléen - - - Masqué - - - Texte - - - Nouveau champ personnalisé - - - Quel type de champ personnalisé souhaitez-vous ajouter ? - - - Supprimer - - - Nouvelle URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domaine de base - - - Par défaut - - - Exact - - - Hôte - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Expression régulière - A programming term, also known as 'RegEx'. - - - Commence par - - - Détection de correspondance URI - - - Détection de correspondance - URI match detection for auto-fill. - - - Oui et enregistrer - - - Autocomplétion et enregistrer - - - Organisation - An entity of multiple related people (ex. a team or business organization). - - - Gardez votre Yubikey près du haut de l'appareil. - - - Réessayez - - - Pour continuer, gardez votre Yubikey NEO à l'arrière de l'appareil. - - - Le service d'accessibilité peut être utile lorsque des applis ne supportent pas le service d'autocomplétion de base. - - - Mot de passe mis à jour - ex. Date this password was updated - - - Mis à jour - ex. Date this item was updated - - - Remplissage automatique activé ! - - - Vous devez vous identifier sur l'application principale Bitwarden avant d'utiliser le remplissage automatique des mots de passe. - - - Vos identifiants sont désormais facilement accessibles directement depuis votre clavier lorsque vous vous connectez à des applications et des sites web. - - - Nous vous recommandons de désactiver toutes les autres applications disponibles dans "Préremplir mots de passe" dans les Réglages d'iOS si vous n'avez pas l'intention de les utiliser. - - - Accédez à votre coffre directement à partir de votre clavier pour remplir automatiquement et rapidement les mots de passe. - - - Pour activer le remplissage automatique des mots de passe sur votre appareil, suivez ces instructions : - - - 1. Allez dans l'application "Réglages" d'iOS - - - 2. Appuyez sur "Mots de passes et comptes" - - - 3. Appuyez sur "Préremplir mots de passe" - - - 4. Activez "Préremplir mots de passe" - - - 5. Sélectionnez "Bitwarden" - - - Remplissage automatique des mots de passe - - - La façon la plus simple d'ajouter de nouveaux identifiants à votre coffre est d'utiliser l'extension "Préremplir mots de passe" de Bitwarden. Pour en savoir plus sur l'utilisation de l'extension "Préremplir mots de passe" de Bitwarden, accédez à l'écran "Outils". - - diff --git a/src/App/Resources/AppResources.hi.Designer.cs b/src/App/Resources/AppResources.hi.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.hi.resx b/src/App/Resources/AppResources.hi.resx deleted file mode 100644 index 173e6c59a..000000000 --- a/src/App/Resources/AppResources.hi.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - About - - - Add - Add/create a new entity (verb). - - - Add Folder - - - Add Item - The title for the add item page. - - - An error has occurred. - Alert title when something goes wrong. - - - Back - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - Cancel - Cancel an operation. - - - Copy - Copy some value to your clipboard. - - - Copy Password - The button text that allows a user to copy the login's password to their clipboard. - - - Copy Username - The button text that allows a user to copy the login's username to their clipboard. - - - Credits - Title for page that we use to give credit to resources that we use. - - - Delete - Delete an entity (verb). - - - Deleting... - Message shown when interacting with the server - - - Do you really want to delete? This cannot be undone. - Confirmation alert message when deleteing something. - - - Edit - - - Edit Folder - - - Email - Short label for an email address. - - - Email Address - Full label for a email address. - - - Email Us - - - Email us directly to get help or leave feedback. - - - Enter your PIN code. - - - Favorites - Title for your favorite items in the vault. - - - File a Bug Report - - - Open an issue at our GitHub repository. - - - Use your fingerprint to verify. - - - Folder - Label for a folder. - - - New folder created. - - - Folder deleted. - - - No Folder - Items that have no folder specified go in this special "catch-all" folder. - - - Folders - - - Folder updated. - - - Go To Website - The button text that allows user to launch the website to their web browser. - - - Help and Feedback - - - Hide - Hide a secret value that is currently shown (password). - - - Please connect to the internet before continuing. - Description message for the alert when internet connection is required to continue. - - - Internet Connection Required - Title for the alert when internet connection is required to continue. - - - Invalid Master Password. Try again. - - - Invalid PIN. Try again. - - - Launch - The button text that allows user to launch the website to their web browser. - - - Log In - The login button text (verb). - - - Login - Title for login page. (noun) - - - Log Out - The log out button text (verb). - - - Are you sure you want to log out? - - - Master Password - Label for a master password. - - - More - Text to define that there are more options things to see. - - - My Vault - The title for the vault page. - - - Name - Label for an entity name. - - - No - - - Notes - Label for notes. - - - Ok - Acknowledgement. - - - Password - Label for a password. - - - Save - Button text for a save operation (verb). - - - Saving... - Message shown when interacting with the server - - - Settings - The title for the settings page. - - - Show - Reveal a hidden value (password). - - - Item has been deleted. - Confirmation message after successfully deleting a login. - - - Submit - - - Sync - The title for the sync page. - - - Thank You - - - Tools - The title for the tools page. - - - URI - Label for a uri/url. - - - Use Fingerprint to Unlock - - - Username - Label for a username. - - - The {0} field is required. - Validation message for when a form field is left blank and is required to be entered. - - - {0} has been copied. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verify Fingerprint - - - Verify Master Password - - - Verify PIN - - - Version - - - View - - - Visit Our Website - - - Visit our website to get help, news, email us, and/or learn more about how to use bitwarden. - - - Website - Label for a website. - - - Yes - - - Account - - - Your new account has been created! You may now log in. - - - Add an Item - - - App Extension - - - Use the bitwarden accessibility service to auto-fill your logins across apps and the web. - - - Auto-fill Service - - - Avoid Ambiguous Characters - - - bitwarden App Extension - - - The easiest way to add new logins to your vault is from the bitwarden App Extension. Learn more about using the bitwarden App Extension by navigating to the "Tools" screen. - - - Use bitwarden in Safari and other apps to auto-fill your logins. - - - bitwarden Auto-fill Service - - - Use the bitwarden accessibility service to auto-fill your logins. - - - Change Email - - - You can change your email address on the bitwarden.com web vault. Do you want to visit the website now? - - - Change Master Password - - - You can change your master password on the bitwarden.com web vault. Do you want to visit the website now? - - - Close - - - Coming Soon! - - - Continue - - - Copied! - - - Copied password! - - - Copied username! - - - Create Account - - - Creating account... - Message shown when interacting with the server - - - Edit Item - - - Enable Automatic Syncing - - - Enter your account email address to receive your master password hint. - - - Re-enable App Extension - - - Almost done! - - - Enable App Extension - - - In Safari, find bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu). - Safari is the name of apple's web browser - - - Get instant access to your passwords! - - - You're ready to log in! - - - See Supported Apps - - - Your logins are now easily accessable from Safari, Chrome, and other supported apps. - - - In Safari and Chrome, find bitwarden using the share icon (hint: scroll to the right on the bottom row of the share menu). - - - Tap the bitwarden icon in the menu to launch the extension. - - - To turn on bitwarden in Safari and other apps, tap the "more" icon on the bottom row of the menu. - - - Favorite - - - Fingerprint - - - Generate Password - - - Get your master password hint - - - Import Items - - - You can bulk import items from the bitwarden.com web vault. Do you want to visit the website now? - - - Quickly bulk import your items from other password management apps. - - - Last Sync: - - - Length - - - Lock - - - 15 minutes - - - 1 hour - - - 1 minute - - - 4 hours - - - Immediately - - - Lock Options - - - Logging in... - Message shown when interacting with the server - - - Log in or create a new account to access your secure vault. - - - Manage - - - Password confirmation is not correct. - - - The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it. - - - Master Password Hint (optional) - - - A master password hint can help you remember your password if you forget it. - - - Master password must be at least 8 characters long. - - - Minimum Numbers - Minimum numeric characters for password generator settings - - - Minimum Special - Minimum special characters for password generator settings - - - More Settings - - - You must log into the main bitwarden app before you can use the extension. - - - Never - - - New item created. - - - There are no favorites in your vault. - - - There are no items in your vault. - - - There are no items in your vault for this website. Tap to add one. - - - This login does not have a username or password configured. - - - Ok, got it! - Confirmation, like "Ok, I understand it" - - - Option defaults are set from the main bitwarden app's password generator tool. - - - Options - - - Other - - - Password generated. - - - Password Generator - - - Password Hint - - - We've sent you an email with your master password hint. - - - Are you sure you want to overwrite the current password? - - - bitwarden keeps your vault automatically synced by using push notifications. For the best possible experience, please select "Ok" on the following prompt when asked to enable push notifications. - Push notifications for apple products - - - Rate the App - - - Please consider helping us out with a good review! - - - App Store ratings are reset with every new version of bitwarden. Please consider helping us out with a good review! - - - Regenerate Password - - - Re-type Master Password - - - Search vault - - - Security - - - See Development Progress - - - Select - - - Set PIN - - - Enter a 4 digit PIN code to unlock the app with. - - - Item Information - - - Item updated. - - - Submitting... - Message shown when interacting with the server - - - Syncing... - Message shown when interacting with the server - - - Syncing complete. - - - Syncing failed. - - - Sync Vault Now - - - Touch ID - What Apple calls their fingerprint reader. - - - Two-step Login - - - Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now? - - - Unlock with {0} - - - Unlock with PIN Code - - - Validating - Message shown when interacting with the server - - - Verification Code - - - View Item - - - bitwarden Web Vault - - - Manage your logins from any web browser with the bitwarden web vault. - - - Lost authenticator app? - - - Items - Screen title - - - Extension Activated! - - - Icons - - - Translations - - - Items for {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - There are no items in your vault for {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - When you see a bitwarden auto-fill notification, you can tap it to launch the auto-fill service. - - - Tap this notification to auto-fill a login from your vault. - - - Open Accessibility Settings - - - 1. On the Android Accessibility Settings screen, touch "bitwarden" under the Services heading. - - - 2. Switch on the toggle and press OK to accept. - - - Disabled - - - Enabled - - - Status - - - Beta - - - The easiest way to add new logins to your vault is from the bitwarden Auto-fill Service. Learn more about using the bitwarden Auto-fill Service by navigating to the "Tools" screen. - - - Auto-fill - - - Do you want to auto-fill or view this login? - - - Are you sure you want to auto-fill this login? It is not a complete match for "{0}". - - - Matching Items - - - Possible Matching Items - - - Search - - - You are searching for an auto-fill login for "{0}". - - - Share Your Vault - - - Create an organization to securely share your logins with other users. - - - Scan When Password Field Focused - - - Only scan the screen for fields and offer an auto-fill notification whenever you select a password field. This setting may help conserve battery life. - - - Persist Notification - - - Always offer an auto-fill notification and only scan for fields after attempting an auto-fill. This setting may help conserve battery life. - - - Always Scan - - - Always scan the screen for fields and only offer an auto-fill notification if password fields are found. This is the default setting. - - - Cannot open the app "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Authenticator App - For 2FA - - - Enter the 6 digit verification code from your authenticator app. - For 2FA - - - Enter the 6 digit verification code that was emailed to {0}. - For 2FA - - - Login Unavailable - For 2FA whenever there are no available providers on this device. - - - This account has two-step login enabled, however, none of the configured two-step providers are supported on this device. Please use a supported device and/or add additional providers that are better supported across devices (such as an authenticator app). - - - Recovery Code - For 2FA - - - Remember me - Remember my two-step login - - - Send verification code email again - For 2FA - - - Two-step Login Options - - - Use another two-step login method - - - Could not send verification email. Try again. - For 2FA - - - Verification email sent. - For 2FA - - - Hold your YubiKey NEO against the back of the device to continue. - - - YubiKey NEO Security Key - "YubiKey" is the product name and should not be translated. - - - Add New Attachment - - - Attachments - - - Unable to download file. - - - Your device cannot open this type of file. - - - Downloading... - Message shown when downloading a file - - - This attachment is {0} in size. Are you sure you want to download it onto your device? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Authenticator Key (TOTP) - - - Verification Code (TOTP) - Totp code label - - - Authenticator key added. - - - Cannot read authenticator key. - - - Scanning will happen automatically. - - - Point your camera at the QR code. - - - Scan QR Code - - - Camera - - - Photos - - - Copied TOTP! - - - Copy TOTP - - - If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login. - - - Disable Automatic TOTP Copy - - - A premium membership is required to use this feature. - - - Attachment added - - - Attachment deleted - - - Choose File - - - File - - - No file chosen - - - There are no attachments. - - - File Source - - - Feature Unavailable - - - Maximum file size is 100 MB. - - - You cannot use this feature until you update your encryption key. - - - Learn More - - - API Server URL - - - Custom Environment - - - For advanced users. You can specify the base URL of each service independently. - - - The environment URLs have been saved. - - - {0} is not correctly formatted. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identity Server URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Self-hosted Environment - - - Specify the base URL of your on-premise hosted bitwarden installation. - - - Server URL - - - Web Vault Server URL - - - Tap this notification to view logins from your vault. - - - Custom Fields - - - Copy Number - - - Copy Security Code - - - Number - - - Security Code - - - What type of item do you want to add? - - - Card - - - Identity - - - Login - - - Secure Note - - - Address 1 - - - Address 2 - - - Address 3 - - - April - - - August - - - Brand - - - Cardholder Name - - - City / Town - - - Company - - - Country - - - December - - - Dr - - - Expiration Month - - - Expiration Year - - - February - - - First Name - - - January - - - July - - - June - - - Last Name - - - License Number - - - March - - - May - - - Middle Name - - - Mr - - - Mrs - - - Ms - - - November - - - October - - - Passport Number - - - Phone - - - September - - - Social Security Number - - - State / Province - - - Title - - - Zip / Postal Code - - - Address - - - Expiration - - - Disable Website Icons - - - Website Icons provide a recognizable image next to each login item in your vault. - - - Icons Server URL - - - Auto-fill with bitwarden - - - Vault is locked - - - Go to my vault - - - Collections - - - There are no items in this collection. - - - There are no items in this folder. - - - Auto-fill Accessibility Service - - - The bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. - - - Use the bitwarden accessibility service to auto-fill your logins. - - - Open Autofill Settings - - - Face ID - What Apple calls their facial recognition reader. - - - Use Face ID to verify. - - - Use Face ID To Unlock - - - Verify Face ID - - - Unlock with Windows Hello - - - Verify with Windows Hello - - - Windows Hello - - - We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings > System > Languages and input > Advanced > Autofill service. - - - Custom Field Name - - - Boolean - - - Hidden - - - Text - - - New Custom Field - - - What type of custom field do you want to add? - - - Remove - - - New URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Base domain - - - Default - - - Exact - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regular expression - A programming term, also known as 'RegEx'. - - - Starts with - - - URI Match Detection - - - Match Detection - URI match detection for auto-fill. - - - Yes, and Save - - - Auto-fill and save - - - Organization - An entity of multiple related people (ex. a team or business organization). - - - Hold your Yubikey near the top of the device. - - - Try Again - - - To continue, hold your YubiKey NEO against the back of the device. - - - The accessibility service may be helpful to use when apps do not support the standard auto-fill service. - - - Password Updated - ex. Date this password was updated - - - Updated - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.hr.Designer.cs b/src/App/Resources/AppResources.hr.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.hr.resx b/src/App/Resources/AppResources.hr.resx deleted file mode 100644 index b15202ae8..000000000 --- a/src/App/Resources/AppResources.hr.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - O aplikaciji - - - Dodaj - Add/create a new entity (verb). - - - Dodaj mapu - - - Dodaj stavku - The title for the add item page. - - - Došlo je do pogreške. - Alert title when something goes wrong. - - - Natrag - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Odustani - Cancel an operation. - - - Kopiraj - Copy some value to your clipboard. - - - Kopiraj lozinku - The button text that allows a user to copy the login's password to their clipboard. - - - Kopiranje korisničko ime - The button text that allows a user to copy the login's username to their clipboard. - - - Zasluge - Title for page that we use to give credit to resources that we use. - - - Izbriši - Delete an entity (verb). - - - Brisanje... - Message shown when interacting with the server - - - Doista želite izbrisati? To se ne može poništiti. - Confirmation alert message when deleteing something. - - - Uredi - - - Uredi mapu - - - E-pošta - Short label for an email address. - - - Adresa e-pošte - Full label for a email address. - - - Pošaljite nam e-poštu - - - Pošaljite nam izravno e-poštu kako biste dobili pomoć ili ostavite povratnu informaciju. - - - Unesite PIN kod. - - - Favoriti - Title for your favorite items in the vault. - - - Pošaljite Izvješće o pogrešci - - - Otvorite problem u našem GitHub repozitoriju. - - - Koristite vaš otisak prsta za potvrdu. - - - Mapa - Label for a folder. - - - Stvorena nova mapa. - - - Mapa obrisana. - - - Nema mape - Items that have no folder specified go in this special "catch-all" folder. - - - Mape - - - Mapa ažurirana. - - - Idite na web stranicu - The button text that allows user to launch the website to their web browser. - - - Pomoć i povratne informacije - - - Sakrij - Hide a secret value that is currently shown (password). - - - Povežite se na internet prije nastavka. - Description message for the alert when internet connection is required to continue. - - - Potrebna je internetska veza - Title for the alert when internet connection is required to continue. - - - Neispravna glavna lozinka. Pokušajte ponovo. - - - Neispravan PIN. Pokušajte ponovo. - - - Pokreni - The button text that allows user to launch the website to their web browser. - - - Prijavi se - The login button text (verb). - - - Prijava - Title for login page. (noun) - - - Odjavi se - The log out button text (verb). - - - Jeste li sigurni da se želite odjaviti? - - - Glavna lozinka - Label for a master password. - - - Više - Text to define that there are more options things to see. - - - Moj trezor - The title for the vault page. - - - Ime - Label for an entity name. - - - Ne - - - Bilješke - Label for notes. - - - U redu - Acknowledgement. - - - Lozinka - Label for a password. - - - Spremi - Button text for a save operation (verb). - - - Spremanje... - Message shown when interacting with the server - - - Postavke - The title for the settings page. - - - Prikaži - Reveal a hidden value (password). - - - Stavka je izbrisana. - Confirmation message after successfully deleting a login. - - - Pošalji - - - Sinkronizacija - The title for the sync page. - - - Hvala Vam - - - Alati - The title for the tools page. - - - URL - Label for a uri/url. - - - Koristite otisak prsta za otključavanje - - - Korisničko ime - Label for a username. - - - Polje {0} je obavezno. - Validation message for when a form field is left blank and is required to be entered. - - - {0} je kopiran. - Confirmation message after suceessfully copying a value to the clipboard. - - - Potvrdite otisak prsta - - - Potvrdite glavnu lozinku - - - Potvrdite PIN - - - Verzija - - - Prikaz - - - Posjetite našu web stranicu - - - Posjetite našu web stranicu kako biste dobili pomoć, vijesti, e-poštu i / ili saznali više o tome kako koristiti Bitwarden. - - - Web stranica - Label for a website. - - - Da - - - Račun - - - Vaš novi račun je kreiran! Sada se možete prijaviti. - - - Dodaj stavku - - - Proširenje aplikacije - - - Koristi usluge pristupačnosti aplikacije Bitwarden za automatsko popunjavanje prijava putem aplikacija i weba. - - - Usluga automatskog popunjavanja - - - Izbjegavajte dvosmislene znakove - - - Bitwarden Proširenje aplikacije - - - Najlakši način za dodavanje novih prijava u Vaš trezor je iz proširenja aplikacije Bitwarden. Saznajte više o upotrebi proširenja aplikacije Bitwarden odlaskom na zaslon "Alati". - - - Upotrijebite Bitwarden u Safariju i drugim aplikacijama da biste automatski ispunili prijave. - - - Bitwarden usluga automatskog popunjavanja - - - Upotrijebite uslugu pristupačnosti usluge Bitwarden da biste automatski ispunili prijave. - - - Promijeni e-poštu - - - Adresu e-pošte možete promijeniti na trezoru web sučelja bitwarden.com. Želite li sada posjetiti web stranicu? - - - Promjena glavne lozinke - - - Možete promijeniti svoju glavnu lozinku na internetskom trezoru bitwarden.com. Želite li sada posjetiti web stranicu? - - - Zatvoriti - - - Dolazi uskoro! - - - Nastaviti - - - Kopirano! - - - Kopirana lozinka! - - - Kopirano korisničko ime! - - - Stvori račun - - - Stvaranje računa... - Message shown when interacting with the server - - - Uredi stavku - - - Omogućite automatsku sinkronizaciju - - - Unesite adresu e-pošte svog računa da biste zaprimili podsjetnik za glavnu lozinku. - - - Ponovno omogućite proširenje aplikacije - - - Skoro gotovo! - - - Omogući proširenje aplikacije - - - U Safariju, pronađite Bitwarden pomoću ikone dijeljenja (savjet: pomaknite se desno u donjem retku izbornika). - Safari is the name of apple's web browser - - - Dobivanje trenutnog pristupa vašim lozinkama! - - - Spremni ste se prijaviti! - - - Pogledajte Podržane aplikacije - - - Vašim prijava se sada lako može pristupiti iz Safaria, Chromea i drugih podržanih aplikacija. - - - U pregledniku Safari i Chrome pronađite Bitwarden upotrebom ikone za dijeljenje (savjet: pomaknite se desno u donjem retku izbornika za dijeljenje). - - - Dodirnite Bitwarden ikonu u izborniku da biste pokrenuli proširenje. - - - Da biste uključili Bitwarden u Safariu i drugim aplikacijama, dodirnite ikonu "više" u donjem retku izbornika. - - - Favorit - - - Otisak prsta - - - Generiraj lozinku - - - Dobijte podsjetnik za glavnu lozinku - - - Uvoz stavki - - - Možete masovno uvesti prijave sa web-trezora bitwarden.com. Želite li sada posjetiti web stranicu? - - - Brzo uvezite svoje prijave iz drugih aplikacija za upravljanje lozinkama. - - - Posljednja sinkronizacija: - - - Dužina - - - Zaključaj - - - 15 minuta - - - 1 sat - - - 1 minuta - - - 4 sata - - - Odmah - - - Opcije zaključavanja - - - Prijavljivanje... - Message shown when interacting with the server - - - Prijavite se ili stvorite novi račun kako biste pristupili svom sigurnom trezoru. - - - Upravljanje - - - Potvrda lozinke nije točna. - - - Glavna lozinka je lozinka koju koristite za pristup vašem trezoru. Vrlo je važno da ne zaboravite glavnu lozinku. Ne postoji način za oporavak lozinke u slučaju da je zaboravite. - - - Podsjetnik glavne lozinke (izborno) - - - Podsjetnik glavne lozinke Vam može pomoći da se prisjetite svoje lozinke ako je zaboravite. - - - Glavna lozinka mora imati najmanje 8 znakova. - - - Najmanje brojeva - Minimum numeric characters for password generator settings - - - Najmanje specijalnih - Minimum special characters for password generator settings - - - Više postavki - - - Morate se prijaviti u glavnu Bitwarden aplikaciju prije nego što možete upotrijebiti proširenje. - - - Nikad - - - Stvorena je nova stavka. - - - U vašem trezoru nema favorita. - - - U Vašem trezoru nema prijava. - - - U Vašem trezoru nema prijava za ovu web stranicu. Dodirnite da biste je dodali. - - - Ova prijava nema definirano korisničko ime ili zaporku. - - - Ok, razumijem! - Confirmation, like "Ok, I understand it" - - - Opcije zadanih postavki postavljene su iz alata generatora lozinke glavne aplikacije Bitwarden. - - - Opcije - - - Ostalo - - - Lozinka generirana. - - - Generator lozinke - - - Podsjetnik za lozinku - - - Poslali smo vam e-poštu s podsjetnikom glavne lozinke. - - - Jeste li sigurni da želite prebrisati trenutnu zaporku? - - - Bitwarden automatski sinkronizira Vaš trezor pomoću push obavijesti. Za najbolji mogući doživljaj, molimo odaberite "Ok" u sljedećem upitu kada se zatraži da omogućite push obavijesti. - Push notifications for apple products - - - Ocijenite aplikaciju - - - Molimo razmislite o tome da nam pomognete s dobrom recenzijom! - - - Ocjene na trgovini aplikacija resetiraju se svakom novom verzijom Bitwardena. Molimo razmislite o tome da nam pomognete s dobrom recenzijom! - - - Obnovi lozinku - - - Ponovno upišite glavnu lozinku - - - Pretraživanje trezora - - - Sigurnost - - - Pogledajte napredak razvoja - - - Odaberi - - - Postavi PIN - - - Unesite četveroznamenkasti PIN kôd za otključavanje aplikacije. - - - Informacije o stavci - - - Stavka je ažurirana. - - - Slanje... - Message shown when interacting with the server - - - Sinkronizacija... - Message shown when interacting with the server - - - Sinkronizacija dovršena. - - - Sinkronizacija nije uspjela. - - - Sinkronizirajte trezor sada - - - Touch ID - What Apple calls their fingerprint reader. - - - Prijava u dva koraka - - - Prijava u dva koraka čini vaš račun sigurnijim tako što će zahtijevati da potvrdite prijavu putem drugog uređaja pomoću sigurnosnog koda, aplikacije autentifikatora, SMS porukom, pozivom ili adresom e-pošte. Prijava u dva koraka može se omogućiti na internetskom trezoru bitwarden.com. Želite li sada posjetiti web stranicu? - - - Otključaj s {0} - - - Otključaj PIN kodom - - - Potvrđivanje - Message shown when interacting with the server - - - Kod za provjeru - - - Prikaz stavke - - - Bitwarden Web trezor - - - Upravljajte svojim prijavama s bilo kojeg web-preglednika pomoću Bitwarden web trezora. - - - Izgubljena aplikacija autentifikatora? - - - Stavke - Screen title - - - Proširenje je aktivirano! - - - Ikone - - - Prijevodi - - - Stavke za {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - U vašem trezoru nema stavki za {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Kada vidite Bitwarden obavijest o automatskom ispunjavanju, možete ju dodirnuti da biste pokrenuli uslugu automatskog popunjavanja. - - - Dodirnite ovu obavijest da biste automatski ispunili prijavu iz svog trezora. - - - Otvorite Postavke pristupačnosti - - - 1. Na zaslonu Postavki pristupačnosti, dodirnite "Bitwarden" ispod naslova usluge. - - - 2. Uključite prekidač i pritisnite OK za prihvaćanje. - - - Onemogućeno - - - Omogućeno - - - Status - - - Beta - - - Najlakši način za dodavanje novih prijava na Vaš trezor je iz usluge s automatskim popunjavanjem Bitwarden. Saznajte više o upotrebi usluge automatskog popunjavanja usluge Bitwarden odlaskom na zaslon "Alati". - - - Automatsko popunjavanje - - - Želite li automatski ispuniti ili pogledati ovu prijavu? - - - Jeste li sigurni da želite automatsko popunjavanje ove prijave? Nije potpuno podudaranje sa "{0}". - - - Podudarajuće stavke - - - Moguće podudaranje stavki - - - Traži - - - Tražite prijavu za automatsko popunjavanje za "{0}". - - - Podijelite trezor - - - Izradite organizaciju kako biste sigurno dijelili svoje prijave s drugim korisnicima. - - - Skeniraj kada se fokusira na polje za lozinku - - - Samo skeniraj zaslon za polja i ponudi obavijest o automatskom ispunjavanju kad god odaberete polje za unos lozinke. Ova postavka vam može pomoći u održavanju trajanja baterije. - - - Trajna obavijest - - - Uvijek nudi obavijest o automatskom ispunjavanju i samo pretraži polja nakon pokušaja automatskog popunjavanja. Ova postavka vam može pomoći u održavanju trajanja baterije. - - - Uvijek skeniraj - - - Uvijek skeniraj zaslon za polja i nudi samo obavijest o automatskom ispunjavanju ako se pronađu polja za unos lozinke. Ovo je zadana postavka. - - - Nije moguće otvoriti aplikaciju "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplikacija autentifikatora - For 2FA - - - Unesite 6-znamenkasti kontrolni kôd iz aplikacije autentifikatora. - For 2FA - - - Unesite 6-znamenkasti kontrolni kôd koji je poslan e-poštom na {0}. - For 2FA - - - Prijava nije dostupna - For 2FA whenever there are no available providers on this device. - - - Ovaj račun ima omogućenu prijavu u dva koraka, međutim, nijedan konfigurirani pružatelj u dva koraka nije podržan na ovom uređaju. Koristite podržani uređaj i/ili dodajte dodatne davatelje koji su bolje podržani u svim uređajima (kao što je aplikacija autentifikatora). - - - Kôd za oporavak - For 2FA - - - Zapamti me - Remember my two-step login - - - Ponovno pošaljite kontrolni kod na adresu e-pošte - For 2FA - - - Mogućnosti prijave u dva koraka - - - Koristite još jednu metodu prijave u dva koraka - - - Nije moguće poslati verifikacijski e-mail. Molim pokušajte ponovno. - For 2FA - - - Verifikacijski e-mail je poslan. - For 2FA - - - Prislonite YubiKey NEO na stražnju stranu uređaja i nastavite. - - - YubiKey NEO sigurnosni ključ - "YubiKey" is the product name and should not be translated. - - - Dodavanje novog privitka - - - Privitci - - - Nije moguće preuzeti datoteku. - - - Uređaj ne može otvoriti ovu vrstu datoteke. - - - Preuzimanje... - Message shown when downloading a file - - - Ovaj prilog je velik {0}. Jeste li sigurni da ga želite preuzeti na uređaj? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Ključ autentifikatora (TOTP) - - - Kod za provjeru (TOTP) - Totp code label - - - Ključ autentifikatora je dodan. - - - Nije moguće pročitati ključ za provjeru autentičnosti. - - - Skeniranje će se dogoditi automatski. - - - Usmjerite kameru na QR kod. - - - Skeniraj QR kôd - - - Kamera - - - Fotografije - - - TOTP kopiran! - - - Kopiraj TOTP - - - Ako uz Vaše podatke za prijavu postoji i autentifikacijski ključ, kontrolni kôd TOTP automatski se kopira u međuspremnik svaki put kada automatski popunite prijavu. - - - Onemogući automatsko kopiranje TOTP - - - Za korištenje ove značajke potrebno je premium članstvo. - - - Prilog dodan - - - Prilog obrisan - - - Odaberite datoteku - - - Datoteka - - - Niste odabrali niti jednu datoteku - - - Nema privitaka. - - - Izvor datoteke - - - Značajka nije dostupna - - - Maksimalna veličina datoteke je 100 MB. - - - Ne možete koristiti ovu značajku dok ne ažurirate ključ za šifriranje. - - - Saznaj više - - - API poslužiteljskog URL-a - - - Prilagođeno okruženje - - - Za napredne korisnike. Samostalno možete odrediti osnovni URL svake usluge. - - - URL-ovi okoline su spremljeni. - - - {0} nije ispravno formatiran. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL identiteta poslužitelja - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Vlastito hosting okruženje - - - Navedite osnovni URL Vaše lokalno hostirane bitwarden instalacije. - - - URL poslužitelja - - - URL poslužitelja web trezora - - - Dodirnite ovu obavijest za pregled prijava iz trezora. - - - Prilagođena polja - - - Kopiraj broj - - - Kopiraj sigurnosni kod - - - Broj - - - Sigurnosni kôd - - - Koju vrstu stavke želite dodati? - - - Kartica - - - Identitet - - - Prijava - - - Sigurna bilješka - - - Adresa 1 - - - Adresa 2 - - - Adresa 3 - - - Travanj - - - Kolovoz - - - Marka - - - Ime vlasnika kartice - - - Grad / Mjesto - - - Tvrtka - - - Zemlja - - - Prosinac - - - dr. - - - Mjesec isteka - - - Godina isteka - - - Veljača - - - Ime - - - Siječanj - - - Srpanj - - - Lipanj - - - Prezime - - - Broj licence - - - Ožujak - - - Svibanj - - - Srednje ime - - - G. - - - Gđa - - - Gđica - - - Studeni - - - Listopad - - - Broj putovnice - - - Telefon - - - Rujan - - - Broj socijalnog osiguranja - - - Država / Pokrajina - - - Titula - - - Poštanski broj - - - Adresa - - - Istek - - - Onemogući ikone web mjesta - - - Ikone web mjesta nude prepoznatljivu ikonu pokraj svake stavke za prijavu u vašem trezoru. - - - URL poslužitelja ikona - - - Automatsko ispunjavanje pomoću Bitwardena - - - Trezor je zaključan - - - Idi na moj trezor - - - Zbirke - - - Nema stavki u ovoj zbirci. - - - Nema stavki u ovoj mapi. - - - Usluga pristupačnosti za automatsko popunjavanje - - - Usluga Bitwarden automatskog ispunjavanja koristi Android Autofill Framework da bi pomogla pri ispunjavanju prijava, kreditnih kartica i identifikacijskih podataka u drugim aplikacijama na Vašem uređaju. - - - Upotrijebite Bitwarden uslugu automatskog popunjavanja prijava, kreditnih kartica i informacija o identitetu u drugim aplikacijama. - - - Otvori postavke automatskog ispunjavanja - - - Face ID - What Apple calls their facial recognition reader. - - - Koristi Face ID za verifikaciju. - - - Koristi Face ID za otključavanje - - - Potvrdite Face ID - - - Otključaj s Windows Hello - - - Potvrdi s Windows Hello - - - Windows Hello - - - Nismo mogli automatski otvoriti izbornik postavki automatskog popunjavanja za Android. Ručno se možete kretati do izbornika postavki automatskog popunjavanja putem Androidovih postavki > Sustav > Jezici i unos > Napredno > Usluga automatskog popunjavanja. - - - Prilagođeno ime polja - - - Boolean - - - Skriveno - - - Tekst - - - Novo prilagođeno polje - - - Koju vrstu prilagođenog polja želite dodati? - - - Ukloni - - - Novi URL - - - URL {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Primarna domena - - - Zadano - - - Točno - - - Domaćin - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regularni izraz - A programming term, also known as 'RegEx'. - - - Počinje sa - - - Otkrivanje podudaranja URL-a - - - Otkrivanje podudaranja - URI match detection for auto-fill. - - - Da, i spremi - - - Automatski popuni i spremi - - - Organizacija - An entity of multiple related people (ex. a team or business organization). - - - Držite Yubikey blizu vrha uređaja. - - - Pokušajte ponovno - - - Da biste nastavili, držite YubiKey NEO na stražnjoj strani uređaja. - - - Usluga pristupačnosti može biti korisna kada aplikacije ne podržavaju standardnu uslugu automatskog popunjavanja. - - - Lozinka ažurirana - ex. Date this password was updated - - - Ažurirano - ex. Date this item was updated - - - Automatsko popunjavanje aktivirano! - - - Morate se prijaviti u glavnu Bitwarden aplikaciju prije nego možete upotrijebljavati Automatsko popunjavanje. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - Preporučujemo da onemogućite bilo koje druge aplikacije za automatsko popunjavanje u odjeljku Postavke ako ih ne namjeravate koristiti. - - - Pristupite svom trezoru izravno s tipkovnice kako biste brzo popunili lozinke. - - - Da biste omogućili automatsko popunjavanje lozinki na uređaju, slijedite ove upute: - - - 1. Idite na iOS aplikaciju "Postavke" - - - 2. Dodirnite "Lozinke i računi" - - - 3. Dodirnite "Automatsko popunjavanje lozinki" - - - 4. Uključite Automatsko popunjavanje - - - 5. Odaberite "Bitwarden" - - - Automatsko popunjavanje zaporki - - - Najlakši način za dodavanje novih prijava u Vaš trezor je korištenjem Bitwarden proširenja za automatsko popunjavanje lozinki. Saznajte više o korištenju Bitwarden proširenja za automatsko popunjavanje lozinki odlaskom na zaslon "Alati". - - diff --git a/src/App/Resources/AppResources.hu.Designer.cs b/src/App/Resources/AppResources.hu.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.hu.resx b/src/App/Resources/AppResources.hu.resx deleted file mode 100644 index 996ebd865..000000000 --- a/src/App/Resources/AppResources.hu.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Névjegy - - - Hozzáadás - Add/create a new entity (verb). - - - Mappa hozzáadása - - - Elem hozzáadása - The title for the add item page. - - - Hiba történt. - Alert title when something goes wrong. - - - Vissza - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - Mégse - Cancel an operation. - - - Másolás - Copy some value to your clipboard. - - - Jelszó másolása - The button text that allows a user to copy the login's password to their clipboard. - - - Felhasználónév másolása - The button text that allows a user to copy the login's username to their clipboard. - - - Készítők - Title for page that we use to give credit to resources that we use. - - - Törlés - Delete an entity (verb). - - - Törlés... - Message shown when interacting with the server - - - Tényleg törölni akarod? Ezt nem lehet visszavonni. - Confirmation alert message when deleteing something. - - - Szerkesztés - - - Mappa szerkesztése - - - E-mail - Short label for an email address. - - - E-mail cím - Full label for a email address. - - - Küldj e-mailt nekünk - - - Írjon nekünk E-mailt közvetlenül a segítség kéréshez vagy visszajelzés beküldéséhez. - - - Add meg a PIN-kódodat. - - - Kedvencek - Title for your favorite items in the vault. - - - Hiba jelentése - - - Nyiss egy Issuet a mi Gthub repositorynkban. - - - Használd az ujjlenyomatod az ellenőrzéshez. - - - Mappa - Label for a folder. - - - Új mappa létrehozva. - - - A mappa törlése megtörtént. - - - Nincs mappa - Items that have no folder specified go in this special "catch-all" folder. - - - Mappák - - - Mappa frissítve. - - - Menj a weboldalra - The button text that allows user to launch the website to their web browser. - - - Súgó és visszajelzés - - - Elrejt - Hide a secret value that is currently shown (password). - - - Csatlakoz az internetre a folytatás előtt. - Description message for the alert when internet connection is required to continue. - - - Internet kapcsolat szükséges - Title for the alert when internet connection is required to continue. - - - Érvénytelen mesterjelszó. Próbáld újra. - - - Érvénytelen PIN. Próbáld újra. - - - Indítás - The button text that allows user to launch the website to their web browser. - - - Bejelentkezés - The login button text (verb). - - - Bejelentkezés - Title for login page. (noun) - - - Kijelentkezés - The log out button text (verb). - - - Biztos, hogy ki szeretnél jelentkezni? - - - Mesterjelszó - Label for a master password. - - - Továbbiak - Text to define that there are more options things to see. - - - Széfem - The title for the vault page. - - - Név - Label for an entity name. - - - Nem - - - Jegyzetek - Label for notes. - - - Ok - Acknowledgement. - - - Jelszó - Label for a password. - - - Mentés - Button text for a save operation (verb). - - - Mentés... - Message shown when interacting with the server - - - Beállítások - The title for the settings page. - - - Mutat - Reveal a hidden value (password). - - - Elem törölve. - Confirmation message after successfully deleting a login. - - - Mehet - - - Szinkronizálás - The title for the sync page. - - - Köszönjük - - - Eszközök - The title for the tools page. - - - URI - Label for a uri/url. - - - Ujjlenyomat használata feloldáshoz - - - Felhasználónév - Label for a username. - - - A(z) {0} mező megadása kötelező. - Validation message for when a form field is left blank and is required to be entered. - - - {0} a vágólapra másolva. - Confirmation message after suceessfully copying a value to the clipboard. - - - Ujjlenyomat megerősítése - - - Mester jelszó megerősítése - - - PIN megerősítése - - - Verzió - - - Megtekintés - - - Látogasd meg a weboldalunkat - - - Látogasd meg a weboldalunkat segítség kéréshez, hírekért, küldj nekünk e-mailt és/vagy tanuld meg hogyan használd a bitwarden-t. - - - Weboldal - Label for a website. - - - Igen - - - Fiók - - - Fiókodat létrehoztuk. Most már be tudsz jelentkezni. - - - Elem hozzáadása - - - App kiterjesztés - - - A bitwarden kisegítő szolgáltatás használata, mely automatikusan kitölti a bejelentkezési adataidat akár egy app-ban, akár webes felületeken. - - - Automatikus kitöltő szolgáltatás - - - Félreérthető karakterek mellőzése - - - bitwarden alkalmazás - - - A legegyszerűbb módja új bejelentkezési adatok hozzáadásának a széfedhez, a bitwarden alkalmazás. Az alkalmazás használatáról az "Eszközök" képernyőn tudhatsz meg többet. - - - Használd a bitwarden automatikus kitöltő szolgáltatását a Safari-ban és más alkalmazásokban. - - - Bitwarden automatikus kitöltő szolgáltatás - - - A bitwarden kisegítő szolgáltatás használata bejelentkezések automatikus kitöltéséhez. - - - E-mail cím módosítása - - - E-mail címedet a bitwarden.com webes széfében tudod megváltoztatni. Szeretnéd meglátogatni most a weboldalt? - - - Mesterjelszó módosítása - - - Mesterjelszavadat a bitwarden.com webes széfében tudod megváltoztatni. Szeretnéd meglátogatni most a weboldalt? - - - Bezár - - - Hamarosan! - - - Folytatás - - - Másolva! - - - Jelszó másolva! - - - Másolt felhasználónév! - - - Fiók létrehozása - - - Fiók létrehozása... - Message shown when interacting with the server - - - Elem szerkesztése - - - Automatikus szinkronizálás engedélyezése - - - Írd be a fiókod e-mail címét, hogy megkapd a mesterjelszó emlékeztetőt. - - - Alkalmazás újra engedélyezése - - - Mindjárt kész! - - - Bővítmény engedélyezése - - - A Safari-ban a bitwarden-t a megosztás ikon alatt találod (Tipp: a menü alsó sorában gördítsd jobbra). - Safari is the name of apple's web browser - - - Kérj azonnali hozzáférést a jelszavakhoz! - - - Készen állsz a bejelentkezéshez! - - - Lásd: támogatott alkalmazások - - - A bejelentkezéseid mostantól könnyen hozzáférhetők a Safari-ból, Chrome-ból és más támogatott alkalmazásból. - - - A Safari-ban és a Chrome-ban a bitwarden-t a megosztás ikon alatt találod (Tipp: a menü alsó sorában gördítsd jobbra). - - - A bővítmény elindításához bökj a bitwarden ikonra a menüben. - - - A bitwarden Safari-ban és más alkalmazásokban való bekapcsolásához bökj az "Egyéb" ikonra a menü alsó sorában. - - - Kedvenc - - - Ujjlenyomat - - - Jelszó generálása - - - Kérj mesterjelszó emlékeztetőt - - - Elemek importálása - - - Elemek tömeges importálása a bitwarden.com webes széfből. Szeretnéd most meglátogatni a weboldalt? - - - Több bejelentkezés gyors importálása egyszerre egy másik jelszókezelő alkalmazásból. - - - Utolsó szinkronizálás: - - - Hossz - - - Lezárás - - - 15 perc - - - 1 óra - - - 1 perc - - - 4 óra - - - Azonnal - - - Zárolási beállítások - - - Bejelentkezés... - Message shown when interacting with the server - - - Jelentkezz be vagy készíts új fiókot a biztonsági széfed eléréshez. - - - Kezelés - - - A megadott két jelszó nem egyezik. - - - A mesterjelszó az a jelszó amit a széfed eléréséhez fogsz használni. Nagyon fontos, hogy ne felejtsd el a mesterjelszavad, mert nincs lehetőséged visszaállítani ha elfelejtetted. - - - Mesterjelszó emlékeztető (nem kötelező) - - - A mesterjelszó emlékeztető segíthet emlékezni a jelszavadra ha elfejetetted volna. - - - Mesterjelszónak legalább 8 karakter hosszúnak kell lennie. - - - Kevesebb szám - Minimum numeric characters for password generator settings - - - Kevesebb különleges karakter - Minimum special characters for password generator settings - - - További beállítások - - - A bővítmény használata előtt be kell lépned a bitwarden alkalmazásba. - - - Soha - - - Új elem létrehozva. - - - Nincsenek kedvencek a széfedben. - - - Nincsenek elemek a széfedben. - - - Ehhez a weboldalhoz nincs bejelentkezés a széfedben. Bökj ide, ha létrehoznál egyet. - - - Ehhez a bejelentkezéshez nem lett felhasználónév vagy jelszó beállítva. - - - Ok, megvan! - Confirmation, like "Ok, I understand it" - - - A beállítások alapértelmezései beállítva a fő bitwarden alkalmazás jelszó generátor eszközéből. - - - Beállítások - - - Egyéb - - - Jelszó generálva. - - - Jelszó generátor - - - Jelszó emlékeztető - - - Elküldtünk neked egy E-mailt mely tartalmazza a mesterjelszó emlékeztetődet. - - - Biztosan felül akarod írni a jelenlegi jelszót? - - - A bitwarden a push értesítések használatával biztosítja a széfed automatikus szinkronizálását. A lehető legjobb élmény érdekében kérjük, válaszd ki az "Ok" opciót a következő értesítéskor, amikor felkér a push értesítések engedélyezésére. - Push notifications for apple products - - - Az alkalmazás értékelése - - - Egy jó értékeléssel mindig segíted a munkánkat! - - - Az App áruházban az értékelések mindig alaphelyzetre állnak egy új verzió megjelenésével. Egy jó értékeléssel mindig segíted a munkánkat! - - - Jelszó újragenerálása - - - Írd be újra a mesterjelszavad - - - Keresés a széfben - - - Biztonság - - - Fejlesztési folyamat megtekintése - - - Kijelölés - - - PIN beállítása - - - Egy 4 számjegyű PIN-kód beállítása az alkalmazás kinyitásához. - - - Elem információ - - - Elem frissítve. - - - Küldés... - Message shown when interacting with the server - - - Szinkronizálás... - Message shown when interacting with the server - - - Szinkronizálás befejezve. - - - Szinkronizáció sikertelen. - - - Széf szinkronizálása most - - - Touch ID - What Apple calls their fingerprint reader. - - - Kétlépcsős bejelentkezés - - - A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókodat azáltal, hogy meg kell erősítened a bejelentkezésedet egy másik eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefon hívás vagy e-mail. Kétlépcsős bejelentkezést a bitwarden.com webes széfében tudod megváltoztatni. Szeretnéd meglátogatni most a weboldalt? - - - Kinyitás ezzel: {0} - - - Kinyitás PIN-kóddal - - - Hitelesítés - Message shown when interacting with the server - - - Hitelesítő kód - - - Elem megtekintése - - - bitwarden webes széf - - - Kezeld az összes bejelentkezésedet bármely böngészőből a bitwarden webes széfével. - - - Elveszett hitelesítő alkalmazás? - - - Elemek - Screen title - - - Kiterjesztés aktiválva! - - - Ikonok - - - Fordítások - - - Elemek ehhez: {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Nincs mentett bejelentkezési adat ehhez: {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Ha látsz egy bitwarden auto-kitöltés értesítést, érintsd meg azt az automatikus kitöltési szolgáltatás elindításához. - - - Az értesítést megéríntétésvel automatikusan kitöltésre kerül egy bejelentkezés a széfedből. - - - Kisegítő szolgáltatások megnyitása - - - 1. az Android kisegítő szolgáltatások képernyőn válaszd ki a "bitwarden"-t a Szolgáltatások fejléc alatt. - - - 2. A felugró ablakban a jóváhagyáshoz bökj az OK gombra. - - - Letiltva - - - Engedélyezve - - - Állapot - - - Béta - - - A legegyszerűbb módja új bejelentkezési adatok hozzáadásának a bitwarden automatikus kitöltö szolgáltatás. Az automatikus kitöltő szolgáltatás használatáról az "Eszközök" képernyőn tudhatsz meg többet. - - - Automatikus kitöltés - - - Szeretnéd automatikusan kitölteni ezt a belentkezést, vagy megnézed inkább? - - - Biztos, hogy automatikusan kitöltöd ezt a bejelentkezést? Nem egyezik teljesen ehhez: {0}. - - - Megegyező elemek - - - Lehetséges megfelelő tételek - - - Keresés - - - Egy bejelentkezést keresel ehhez: "{0}". - - - Széfed megosztása - - - Hozz létre egy szervezetet, hogy biztonságosan megoszthasd bejelentkezéseidet másokkal. - - - Szkennelés, amikor a jelszó mezőre fókuszált - - - Csak akkor szkennelje az ablakot és ajánljon automatikus kitöltést, amikor a jelszó mező van kijelölve. Ez a beállítás segíthet megnövelni az akku élettartamát. - - - Állandó értestés - - - Mindig mutatja az automatikus kitöltés értesítést és csak az automatikus kitöltés megkísérlése után szkenneli a mezőket. Ez a beállítás segíthet megnövelni az akku élettartamát. - - - Mindig szkennel - - - Mindig szkenneli a mezőket a képernyőn és csak akkor ajánl automatikus kitöltést, ha jelszó mezőt talál. Ez az alapértelmezett beállítás. - - - Nem lehet megnyitni a(z) "{0}" alkalmazást. - Message shown when trying to launch an app that does not exist on the user's device. - - - Hitelesítő alkalmazás - For 2FA - - - Add meg a 6 számjegyű ellenőrző kódot a hitelesítő alkalmazásodból. - For 2FA - - - Add meg a 6 számjegyű ellenőrző kódot ami a(z) {0} e-mail címre lett küldve. - For 2FA - - - Bejelentkezés nem érhető el - For 2FA whenever there are no available providers on this device. - - - Ennél a fióknál a kétlépcsős bejelentkezés engedélyezve lett, azonban a beállított kétlépcsős szolgáltatók egyike sem támogatott ezen az eszközön. Használj támogatott eszközt és/vagy olyan szolgáltatókat, amelyek jobb támogatást élveznek az eszközökön (például egy hitelesítő alkalmazás). - - - Helyreállító kód - For 2FA - - - Emlékezz rám - Remember my two-step login - - - Megerősítő kód újraküldése email-ben - For 2FA - - - Kétlépcsős bejelentkezés opciók - - - Más kétlépcsős bejelentkezés használata - - - Nem sikerült elküldeni a megerősítő e-mailt. Próbáld újra. - For 2FA - - - Megerősítő e-mail elküldve. - For 2FA - - - A folytatáshoz tartsd a YubiKey NEO-t a készülék hátuljához. - - - YubiKey NEO biztonsági kulcs - "YubiKey" is the product name and should not be translated. - - - Melléklet hozzáadása - - - Mellékletek - - - Nem lehet letölteni a fájlt. - - - A készülék nem tudja megnyitni az ilyen típusú fájlt. - - - Letöltés... - Message shown when downloading a file - - - Ez a melléklet {0} méretű. Biztosan le akarod tölteni a készülékedre? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Hitelesítő kulcs (egyszeri-idő alapú) - - - Ellenőrző kód (egyszeri-idő alapú) - Totp code label - - - Hitelesítő kulcs hozzáadva. - - - Nem lehet olvasni a hitelesítő kulcsot. - - - A szkennelés automatikusan megtörténik. - - - Irányítsd a kamerát a QR-kódra. - - - QR kód beolvasása - - - Kamera - - - Képek - - - Egyszeri jelszó másolva! - - - Egyszeri jelszó másolása - - - Ha a bejelentkezésedhez csatolva van egy hitelesítő kód, az egyszeri ellenőrző kód automatikusan másolva lesz a vágólapra a bejelentkezés automatikus kitöltése esetén. - - - Egyszeri jelszó automatikus másolás tiltása - - - A funkció használatához prémium tagság szükséges. - - - Melléklet hozzáadva - - - Melléklet törölve - - - Fájl kiválasztása - - - Fájl - - - Nincs kiválasztott fájl - - - Nincsenek mellékletek. - - - Fájl forrás - - - Ez a funkció nem érhető el - - - Maximális fájl méret 100 MB. - - - Ez a funkció nem használható, amíg nem frissíted a titkosítási kulcsodat. - - - Tudj meg többet - - - API szerver URL - - - Egyéni környezet - - - Haladó felhasználóknak. Minden egyes szolgáltatásnak külön megadhatod az alap URL-t. - - - A környezet URL-ek el lettek mentve. - - - {0} nem megfelelő formátumú. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identitás szerver URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Saját üzemeltetésű környezet - - - Add meg az alap URL-t a helyileg üzemeltetett bitwarden telepítéshez. - - - Szerver URL - - - Webes széf szerver URL - - - Az értesítés megérintésével megnézheted a széfedben tárolt bejelentkezéseket. - - - Egyedi mezők - - - Szám másolása - - - Biztonsági kód másolása - - - Szám - - - Biztonsági Kód - - - Milyen típusú tételt szeretnél hozzáadni? - - - Kártya - - - Azonosság - - - Bejelentkezés - - - Biztonságos jegyzet - - - Cím 1 - - - Cím 2 - - - Cím 3 - - - Április - - - Augusztus - - - Márka - - - Kártyatulajdonos neve - - - Település neve - - - Cég - - - Ország - - - December - - - Dr - - - Lejárati hónap - - - Lejárati év - - - Február - - - Keresztnév - - - Január - - - Július - - - Június - - - Vezetéknév - - - Engedélyszám - - - Március - - - Május - - - Középső név - - - Úr - - - Asszony - - - Kisasszony - - - November - - - Október - - - Útlevélszám - - - Telefonszám - - - Szeptember - - - Társadalombiztosítási szám - - - Megye - - - Titulus - - - Irányítószám - - - Lakcím - - - Lejárat - - - Honlap ikonok letiltása - - - A webhelyek ikonjai felismerhető ikonként jelennek meg a széfed minden eleme mellett. - - - Ikonok kiszolgáló URL-címe - - - Automatikus kitöltés bitwardennel - - - A széf zárolva van - - - Széfem megnyitása - - - Gyűjtemények - - - Nincsenek elemek ebben a gyűjteményben. - - - Nincsenek elemek ebben a mappában. - - - Automatikus kitöltés kisegítő szolgáltatás - - - A bitwarden automatikus kitöltés szolgáltatása az Android automatikus kitöltő keretrendszerét használja a bejelentkezések, hitelkártyák és egyéb azonosító adatok kitöltésére más alkalmazásokban. - - - A bitwarden kisegítő szolgáltatás használata bejelentkezések automatikus kitöltéséhez. - - - Automatikus kitöltő beállítások megnyitása - - - Arc-azonosító - What Apple calls their facial recognition reader. - - - Arc azonosítás használata az ellenőrzéshez. - - - Arc-azonosító használata a kinyitáshoz - - - Arc-azonosító megerősítése - - - Kinyitás Windows Hello-val - - - Ellenőrzés Windows Hello-val - - - Windows Hello - - - Nem sikerült automatikusan megnyitni az Android automatikus kitöltés beállításai menüt. A beállítás megnyitásához nyitsd meg a Beállítások > Rendszer > Nyelv és bevitel > Speciális > Automatikus kitöltés menüpontot. - - - Egyéni mező neve - - - Logikai - - - Rejtett - - - Szöveg - - - Új egyedi mező - - - Milyen típusú egyéni mező szeretné hozzáadni? - - - Eltávolít - - - Új URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Alap domain - - - Alapértelmezett - - - Pontos - - - Hoszt - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Reguláris kifejezés - A programming term, also known as 'RegEx'. - - - Ezzel kezdődik - - - URI találatfelismerés - - - Találatfelismerés - URI match detection for auto-fill. - - - Igen, és mentés - - - Automatikus kitöltés és mentés - - - Szervezet - An entity of multiple related people (ex. a team or business organization). - - - Tartsd lenyomva a Yubikey-t, a készülék tetején. - - - Próbáld újra - - - A folytatáshoz tartsd lenyomva a YubiKey NEO-t a készülék hátoldalán. - - - A kisegítő szolgáltatás használata hasznos lehet, ha az alkalmazások nem támogatják a szabványos automatikus kitöltési szolgáltatást. - - - Jelszó frissítve - ex. Date this password was updated - - - Frissítve - ex. Date this item was updated - - - Automatikus kitöltés aktiválva! - - - Az automatikus kitöltő szolgáltatás használata előtt be kell lépned a fő Bitwarden alkalmazásba. - - - Az alkalmazásokba és weboldalakra a belépési adatok most már könnyedén elérhető a billentyűzetről. - - - Javasoljuk, hogy kapcsolj ki minden más automatikus kitöltő alkalmazást, ha nem használod azokat. - - - A széf közvetlenül a billentyűzetről érhető el a jelszavak gyors kitöltéséhez. - - - A jelszó automatikus kitöltés szolgáltatás engedélyezéséhez kövesd az alábbi instrukciókat: - - - 1. Lépj be az iOS "Beállítások" alkalmazásba - - - 2. Válaszd a "Jelszavak és Fiókok" lehetőséget - - - 3. Válaszd ki az "Jelszavak automatikus kitöltése" lehetőséget - - - 4. Kapcsold be az opciót - - - 5. Válaszd ki a "Bitwarden"-t - - - Jelszó Automatikus kitöltése - - - A legegyszerűbb módja új bejelentkezési adatok hozzáadásának a széfedhez, a bitwarden alkalmazás. Az alkalmazás használatáról az "Eszközök" képernyőn tudhatsz meg többet. - - diff --git a/src/App/Resources/AppResources.id.Designer.cs b/src/App/Resources/AppResources.id.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.id.resx b/src/App/Resources/AppResources.id.resx deleted file mode 100644 index ad5f45b92..000000000 --- a/src/App/Resources/AppResources.id.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Tentang - - - Tambah - Add/create a new entity (verb). - - - Tambah Folder - - - Tambah Item - The title for the add item page. - - - Terjadi kesalahan. - Alert title when something goes wrong. - - - Kembali - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Batal - Cancel an operation. - - - Salin - Copy some value to your clipboard. - - - Salin Kata Sandi - The button text that allows a user to copy the login's password to their clipboard. - - - Salin Nama Pengguna - The button text that allows a user to copy the login's username to their clipboard. - - - Kredit - Title for page that we use to give credit to resources that we use. - - - Hapus - Delete an entity (verb). - - - Menghapus... - Message shown when interacting with the server - - - Apakah Anda yakin ingin menghapusnya? Ini tidak dapat dibatalkan. - Confirmation alert message when deleteing something. - - - Edit - - - Edit Folder - - - Email - Short label for an email address. - - - Alamat Email - Full label for a email address. - - - Kirim Email Ke Kami - - - Email kami secara langsung untuk mendapatkan bantuan atau memberikan umpan balik. - - - Masukkan kode PIN Anda. - - - Favorit - Title for your favorite items in the vault. - - - Buat Laporan Masalah - - - Buka sebuah isu di repositori GitHub kami. - - - Gunakan sidik jari Anda untuk memverifikasi. - - - Folder - Label for a folder. - - - Folder baru telah dibuat. - - - Folder dihapus. - - - Tidak Ada Folder - Items that have no folder specified go in this special "catch-all" folder. - - - Folder - - - Folder diperbarui. - - - Buka Situs Web - The button text that allows user to launch the website to their web browser. - - - Bantuan dan Umpan Balik - - - Sembunyikan - Hide a secret value that is currently shown (password). - - - Pastikan Anda tersambung ke internet sebelum melanjutkan. - Description message for the alert when internet connection is required to continue. - - - Sambungan Internet Dibutuhkan - Title for the alert when internet connection is required to continue. - - - Kata Sandi Utama Tidak Valid. Coba Lagi. - - - PIN Tidak Valid. Coba Lagi. - - - Luncurkan - The button text that allows user to launch the website to their web browser. - - - Masuk - The login button text (verb). - - - Masuk - Title for login page. (noun) - - - Keluar - The log out button text (verb). - - - Anda yakin ingin keluar? - - - Kata Sandi Utama - Label for a master password. - - - Lainnya - Text to define that there are more options things to see. - - - Brankas Saya - The title for the vault page. - - - Nama - Label for an entity name. - - - Tidak - - - Catatan - Label for notes. - - - Oke - Acknowledgement. - - - Kata Sandi - Label for a password. - - - Simpan - Button text for a save operation (verb). - - - Menyimpan... - Message shown when interacting with the server - - - Setelan - The title for the settings page. - - - Tampilkan - Reveal a hidden value (password). - - - Item telah dihapus. - Confirmation message after successfully deleting a login. - - - Kirim - - - Sinkronisasi - The title for the sync page. - - - Terima Kasih - - - Alat - The title for the tools page. - - - URI - Label for a uri/url. - - - Gunakan Sidik Jari untuk Membuka - - - Nama Pengguna - Label for a username. - - - Kolom {0} harus diisi. - Validation message for when a form field is left blank and is required to be entered. - - - {0} disalin. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verifikasi Sidik Jari - - - Verifikasi Kata Sandi Utama - - - Verifikasi PIN - - - Versi - - - Tampilan - - - Kunjungi Situs Web Kami - - - Kunjungi situs web kami untuk mendapatkan bantuan, berita, email kami, dan/atau mempelajari lebih lanjut tentang bagaimana menggunakan Bitwarden. - - - Situs web - Label for a website. - - - Ya - - - Akun - - - Akun baru Anda telah dibuat! Sekarang Anda bisa masuk. - - - Tambah Item - - - Ekstensi Aplikasi - - - Gunakan layanan aksesibilitas Bitwarden untuk mengisi otomatis info masuk Anda di seluruh aplikasi dan web. - - - Layanan Pengisian Otomatis - - - Hindari Karakter Ambigu - - - Ekstensi Aplikasi Bitwarden - - - Cara termudah untuk menambahkan info masuk baru ke brankas Anda adalah dari Ekstensi Aplikasi Bitwarden. Pelajari lebih lanjut tentang menggunakan Ekstensi Aplikasi Bitwarden dengan menavigasi ke layar "Alat". - - - Gunakan Bitwarden di Safari dan aplikasi lainnya untuk mengisi otomatis info masuk Anda. - - - Layanan Pengisian Otomatis Bitwarden - - - Gunakan layanan aksesibilitas Bitwarden untuk mengisi otomatis info masuk Anda. - - - Ubah Email - - - Anda dapat mengubah alamat email Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang? - - - Ubah Kata Sandi Utama - - - Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang? - - - Tutup - - - Segera Hadir! - - - Lanjutkan - - - Tersalin! - - - Kata sandi tersalin! - - - Nama pengguna tersalin! - - - Buat Akun - - - Membuat akun... - Message shown when interacting with the server - - - Edit Item - - - Aktifkan Sinkronisasi Otomatis - - - Masukkan email akun Anda untuk menerima pentunjuk sandi utama Anda. - - - Aktifkan Ulang Ekstensi Aplikasi - - - Hampir selesai! - - - Aktifkan Ekstensi Aplikasi - - - Pada Safari, temukan Bitwarden menggunakan ikon berbagi (petunjuk: gulir ke kanan di baris bawah menu). - Safari is the name of apple's web browser - - - Dapatkan akses instan ke kata sandi Anda! - - - Anda siap untuk masuk! - - - Lihat Aplikasi yang Didukung - - - Login anda sekarang bisa diakses dengan mudah dari Safari, Chrome, dan aplikasi lain yang didukung. - - - Pada Safari dan Chrome, temukan Bitwarden menggunakan ikon berbagi (petunjuk: gulir ke kanan di baris bawah menu). - - - Ketuk ikon Bitwarden pada menu untuk menjalankan ekstensi. - - - Untuk mengaktifkan Bitwarden di Safari dan aplikasi lainnya, ketuk ikon "lebih banyak" di baris bawah menu. - - - Favorit - - - Sidik Jari - - - Buat Kata Sandi - - - Dapatkan petunjuk sandi utama Anda - - - Impor Item - - - Anda dapat mengimpor massal item dari brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang? - - - Mengimpor massal item Anda dari aplikasi pengelola sandi lainnya dengan cepat. - - - Sinkronisasi Terakhir: - - - Panjang - - - Kunci - - - 15 menit - - - 1 jam - - - 1 menit - - - 4 jam - - - Segera - - - Pilihan Penguncian - - - Sedang masuk... - Message shown when interacting with the server - - - Masuk atau buat akun baru untuk mengakses brankas Anda. - - - Kelola - - - Konfirmasi kata sandi tidak benar. - - - Kata sandi utama adalah kata sandi yang Anda gunakan untuk mengakses brankas Anda. Sangat penting bahwa Anda tidak lupa kata sandi utama Anda. Tidak ada cara untuk memulihkan kata sandi jika Anda melupakannya. - - - Petunjuk Kata Sandi Utama (pilihan) - - - Petunjuk kata sandi utama dapat membantu Anda mengingat kata sandi Anda jika Anda melupakannya. - - - Kata sandi utama sedikitnya harus 8 karakter. - - - Angka Minimum - Minimum numeric characters for password generator settings - - - Karakter Khusus Minimum - Minimum special characters for password generator settings - - - Setelan Lainnya - - - Anda harus masuk ke aplikasi utama Bitwarden sebelum Anda dapat menggunakan ekstensi. - - - Jangan pernah - - - Item baru telah dibuat. - - - Tidak ada favorit di brankas Anda. - - - Tidak ada item di brankas Anda. - - - Tidak ada item untuk situs ini di brankas Anda. Ketuk untuk menambahkan. - - - Info masuk ini tidak memiliki nama pengguna atau sandi yang dikonfigurasi. - - - Oke, saya mengerti! - Confirmation, like "Ok, I understand it" - - - Pilihan baku diatur dari alat pembuat sandi utama aplikasi Bitwarden. - - - Pilihan - - - Lainnya - - - Sandi telah dibuat. - - - Pembuat Kata Sandi - - - Petunjuk Sandi - - - Kami telah mengirimi Anda email dengan petunjuk sandi utama Anda. - - - Anda yakin ingin menimpa sandi saat ini? - - - Bitwarden membuat brankas Anda disinkronkan secara otomatis dengan menggunakan notifikasi. Untuk pengalaman yang sebaik mungkin, silakan pilih "Oke" pada saran berikut ketika diminta untuk mengaktifkan notifikasi. - Push notifications for apple products - - - Nilai Aplikasi - - - Mohon pertimbangkan membantu kami dengan ulasan yang baik! - - - Peringkat App Store diatur ulang dengan setiap versi baru dari Bitwarden. Mohon pertimbangkan membatu kami dengan ulasan yang baik! - - - Buat Ulang Sandi - - - Ketik Ulang Sandi Utama - - - Cari Brankas - - - Keamanan - - - Lihat Kemajuan Pengembangan - - - Pilih - - - Atur PIN - - - Masukkan 4 digit kode PIN untuk membuka aplikasi. - - - Informasi Item - - - Item diperbarui. - - - Mengirim... - Message shown when interacting with the server - - - Menyinkronkan... - Message shown when interacting with the server - - - Sinkronisasi selesai. - - - Sinkronisasi gagal. - - - Sinkronkan Brankas Sekarang - - - Touch ID - What Apple calls their fingerprint reader. - - - Info masuk dua langkah - - - Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMK, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang? - - - Buka denga {0} - - - Buka dengan kode PIN - - - Memvalidasi - Message shown when interacting with the server - - - Kode Verifikasi - - - Lihat Item - - - Brankas Web Bitwarden - - - Kelola info masuk Anda dari berbagai peramban web dengan Bitwarden web vault. - - - Kehilangan aplikasi autentikasi? - - - Item - Screen title - - - Ekstensi Diaktifkan! - - - Ikon - - - Terjemahan - - - Item untuk {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Tidak ada item di brankas Anda untuk {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Ketika Anda melihat notifikasi pengisian otomatis bitwarden, Anda dapat mengetuknya untuk meluncurkan layanan pengisian otomatis. - - - Ketuk notifikasi ini untuk mengisi info masuk secara otomatis dari brankas Anda. - - - Buka Setelan Aksesibilitas - - - 1. Pada layar Setelan Aksesibilitas Android, sentuh "bitwarden" di bawah Layanan. - - - 2. Aktifkan sakelar dan tekan OKE untuk menerima. - - - Dinonaktifkan - - - Diaktifkan - - - Status - - - Beta - - - Cara termudah untuk menambahkan log masuk baru ke brankas Anda adalah dari Layanan Pengisian Otomatis bitwarden. Pelajari lebih lanjut tentang menggunakan Layanan Pengisian Otomatis bitwarden dengan menavigasi ke layar "Alat". - - - Isi otomatis - - - Apakah Anda ingin mengisi otomatis atau melihat info masuk ini? - - - Anda yakin ingin mengisi otomatis info masuk ini? Ini tidak sepenuhnya cocok untuk "{0}". - - - Item yang Cocok - - - Item yang Mungkin Cocok - - - Pencarian - - - Anda mencari item pengisian otomatis untuk "{0}". - - - Bagikan Brankas Anda - - - Buat organisasi untuk berbagi info masuk Anda dengan aman dengan pengguna lain. - - - Pindai Ketika Fokus ke Kolom Kata Sandi - - - Hanya pindai layat untuk kolom dan tampilkan notifikasi pengisian otomatis setiap kali Anda memilih kolom kata sandi. Setelan ini mungkin dapat membantu menghemat baterai. - - - Notikasi Tetap - - - Selalu munculkan notifikasi pengisian otomatis dan hanya memindai kolom setelah mencoba pengisian otomatis. Setelan ini mungkin dapat membantu menghemat baterai. - - - Selalu Pindai - - - Selalu pindai layar untuk kolom dan hanya munculkan notifikasi pengisian otomatis jika kolom kata sandi ditemukan. Ini adalah setelan bawaan. - - - Tidak dapat membuka aplikasi {0}. - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplikasi Autentikasi - For 2FA - - - Masukkan 6 digit kode verifikasi dari aplikasi autentikasi Anda. - For 2FA - - - Masukkan 6 digit kode verifikasi yang dikirim melalui email ke {0}. - For 2FA - - - Info Masuk Tidak Tersedia - For 2FA whenever there are no available providers on this device. - - - Akun ini mengaktifkan info masuk dua langkah, namun, tidak satupun dari penyedia dua langkah yang dikonfigurasi didukung oleh peranti ini. Silakan gunakan peranti yang didukung dan/atau tambahkan penyedia tambahan yang didukung di semua peranti (seperti aplikasi autentikasi). - - - Kode Pemulihan - For 2FA - - - Ingat saya - Remember my two-step login - - - Kirim ulang email kode verifikasi - For 2FA - - - Pilihan Info Masuk Dua Langkah - - - Gunakan metode masuk dua langkah lainnya - - - Tidak dapat mengirim email verifikasi. Coba lagi. - For 2FA - - - Email verifikasi terkirim. - For 2FA - - - Untuk melanjutkan, tahan YubiKey Neo pada bagian belakang perangkat anda atau masukkan YubiKey ke port USB perangkat anda, kemudian tekan tombolnya. - - - Kunci Keamanan YubiKey - "YubiKey" is the product name and should not be translated. - - - Tambah Lampiran Baru - - - Lampiran - - - Tidak bisa mengunduh berkas. - - - Peranti Anda tidak dapat membuka jenis berkas ini. - - - Mengunduh... - Message shown when downloading a file - - - Lampiran ini berukuran {0}. Anda ingin mengunduhnya ke peranti Anda? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Kunci Autentikasi (TOTP) - - - Kode Verifikasi (TOTP) - Totp code label - - - Kunci autentikasi ditambahkan. - - - Tidak dapat membaca kunci autentikasi. - - - Pemindaian akan terjadi secara otomatis. - - - Arahkan kamera Anda ke kode QR. - - - Pindai Kode QR - - - Kamera - - - Foto - - - TOTP disalin! - - - Salin TOTP - - - Jika info masuk Anda memiliki kunci autentikasi yang menyertainya, kode verifikasi TOTP akan disalin secara otomatis ke clipboard Anda setiap kali Anda mengisi info masuk secara otomatis. - - - Nonaktifkan salinan TOTP otomatis - - - Keanggotaan premium diperlukan untuk menggunakan fitur ini. - - - Lampiran ditambahkan - - - Lampiran dihapus - - - Pilih Berkas - - - Berkas - - - Tidak ada berkas yang dipilih - - - Tidak ada lampiran. - - - Sumber Berkas - - - Fitur Tidak Tersedia - - - Ukuran berkas maksimal adalah 100 MB. - - - Anda tidak dapat menggunakan fitur ini sampai Anda memperbarui kunci enkripsi Anda. - - - Pelajari Lebih Lanjut - - - URL Server API - - - Lingkungan Kustom - - - Untuk pengguna tingkat lanjutan. Anda dapat menentukan secara mandiri basis dari URL untuk setiap layanan. - - - URL dari lingkungan sudah tersimpan. - - - {0} tidak diformat dengan benar. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL Server Identitas - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Lingkungan Hos-mandiri - - - Specify the base URL of your on-premise hosted bitwarden installation. - - - URL Server - - - URL Server Brankas Web - - - Ketuk notifikasi ini untuk melihat item dari brankas Anda. - - - Kolom Ubahsuai - - - Salin Nomor - - - Salin Kode Keamanan - - - Nomor - - - Kode Keamanan - - - Jenis apa yang ingin Anda tambahkan? - - - Kartu - - - Identitas - - - Info Masuk - - - Catatan Aman - - - Alamat 1 - - - Alamat 2 - - - Alamat 3 - - - April - - - Agustus - - - Merek - - - Nama Pemilik Kartu - - - Kota / Kabupaten - - - Perusahaan - - - Negara - - - Desember - - - Dr - - - Bulan Kedaluwarsa - - - Tahun Kedaluwarsa - - - Februari - - - Nama Depan - - - Januari - - - Juli - - - Juni - - - Nama Belakang - - - Nomor Lisensi - - - Maret - - - Mei - - - Nama Tengah - - - Tuan - - - Nyonya - - - Nona - - - November - - - Oktober - - - Nomor Paspor - - - Telepon - - - September - - - Nomor Jaminan Sosial - - - Negara Bagian / Provinsi - - - Panggilan - - - Kode Pos - - - Alamat - - - Masa Berlaku - - - Nonaktifkan Ikon Situs Web - - - Ikon Situs Web menyediakan gambar yang dikenali di sebelah item info masuk di brankas Anda. - - - URL Server Ikon - - - Isi otomatis dengan Bitwarden - - - Brankas terkunci - - - Buka brankas saya - - - Koleksi - - - Tidak ada item di koleksi ini. - - - Tidak ada item di folder ini. - - - Layanan Aksesibilitas Isi Otomatis - - - Layanan pengisian otomatis bitwarden menggunakan Kerangka Kerja Pengisian Otomatis Android untuk membantu dalam mengisi info masuk, kartu kredit, dan informasi identitas ke aplikasi lain di perangkat Anda. - - - Gunakan layanan pengisian otomatis bitwarden untuk mengisi info masuk, kartu kredit, dan informasi identitas ke aplikasi lainnya. - - - Buka Setelan Isi Otomatis - - - Face ID - What Apple calls their facial recognition reader. - - - Gunakan Face ID untuk memverifikasi. - - - Gunakan Face ID untuk membuka - - - Verifikasi Face ID - - - Buka dengan Windows Hello - - - Verifikasi dengan Windows Hello - - - Windows Hello - - - Kami tidak dapat secara otomatis membuka menu pengaturan Android isi otomatis untuk Anda. Anda dapat membuka menu pengaturan isi otomatis secara manual dari Pengaturan Android > Sistem > Bahasa dan masukan > Lanjutan > Layanan isi otomatis. - - - Nama Kolom Pilihan Sendiri - - - Boolean - - - Tersembunyi - - - Teks - - - Kolom Pilihan Sendiri Baru - - - Jenis kolom ubahsuai apa yang ingin Anda tambahkan? - - - Hapus - - - URl Baru - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domain basis - - - Bawaan - - - Tepat - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Ekspresi umum - A programming term, also known as 'RegEx'. - - - Mulai dengan - - - Deteksi kecocokan URI - - - Deteksi kecocokan - URI match detection for auto-fill. - - - Ya, dan Simpan - - - Isi otomatis dan simpan - - - Organisasi - An entity of multiple related people (ex. a team or business organization). - - - Pegang Yubikey di dekat bagian atas perangkat. - - - Coba Lagi - - - Untuk melanjutkan, pegang YubiKey NEO Anda pada bagian belakang perangkat. - - - Layanan aksesibilitas sangat berguna untuk dimanfaatkan saat aplikasi tidak mendukung layanan isi otomatis. - - - Kata Sandi Diperbarui - ex. Date this password was updated - - - Di perbarui - ex. Date this item was updated - - - Isi Otomatis diaktifkan! - - - Anda harus masuk ke aplikasi utama Bitwarden sebelum Anda dapat menggunakan Isi Otomatis. - - - Sekarang login Anda bisa diakses dengan mudah dari keyboard Anda saat masuk ke aplikasi atau lewat situs web. - - - Sebaiknya fitur Isi Otomatis dinonaktifkan lewat Pengaturan, jika Anda tidak berencana menggunakannya. - - - Mengakses vault Anda lewat keyboard untuk mengisi otomatis sandi dengan cepat. - - - Untuk mengisi otomatis sandi pada perangkat Anda, ikuti petunjuk berikut: - - - 1. Pergi ke "Setting" di menu iOS app - - - 2. Pilih "Password & Accounts" - - - 3. Pilih "AutoFill Passwords" - - - 4. Aktifkan AutoFill - - - 5. Pilih "Bitwarden" - - - Isi Otomatis Sandi - - - Cara termudah untuk menambahkan login baru ke brankas Anda adalah dari ekstensi Isi Otomatis Sandi Bitwarden. Pelajari lebih lanjut penggunaan ekstensi ini dengan mengunjungi layar "Alat". - - diff --git a/src/App/Resources/AppResources.it.Designer.cs b/src/App/Resources/AppResources.it.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.it.resx b/src/App/Resources/AppResources.it.resx deleted file mode 100644 index a127a0c32..000000000 --- a/src/App/Resources/AppResources.it.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Informazioni - - - Aggiungi - Add/create a new entity (verb). - - - Aggiungi Cartella - - - Aggiungi elemento - The title for the add item page. - - - Si è verificato un errore. - Alert title when something goes wrong. - - - Indietro - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Annulla - Cancel an operation. - - - Copia - Copy some value to your clipboard. - - - Copia Password - The button text that allows a user to copy the login's password to their clipboard. - - - Copia Nome Utente - The button text that allows a user to copy the login's username to their clipboard. - - - Crediti - Title for page that we use to give credit to resources that we use. - - - Cancella - Delete an entity (verb). - - - Cancellazione in corso... - Message shown when interacting with the server - - - Vuoi veramente cancellare? Non potrai annullare. - Confirmation alert message when deleteing something. - - - Modifica - - - Modifica Cartella - - - Email - Short label for an email address. - - - Indirizzo Email - Full label for a email address. - - - Contattaci - - - Mandaci una Email per ottenere aiuto o lasciare un feedback. - - - Inserisci il tuo PIN. - - - Preferiti - Title for your favorite items in the vault. - - - Notificaci un Bug - - - Apri un issue nel nostro repository GitHub. - - - Usa la tua impronta per la verifica. - - - Cartella - Label for a folder. - - - Nuova cartella creata. - - - Cartella eliminata. - - - Nessuna Cartella - Items that have no folder specified go in this special "catch-all" folder. - - - Cartelle - - - Cartella aggiornata. - - - Vai al sito - The button text that allows user to launch the website to their web browser. - - - Aiuto e feedback - - - Nascondi - Hide a secret value that is currently shown (password). - - - Per favore connettiti a internet prima di continuare. - Description message for the alert when internet connection is required to continue. - - - Connessione ad internet richiesta - Title for the alert when internet connection is required to continue. - - - Password Principale non valida. Riprova. - - - PIN errato. Riprova. - - - Avvia - The button text that allows user to launch the website to their web browser. - - - Log In - The login button text (verb). - - - Login - Title for login page. (noun) - - - Disconnetti - The log out button text (verb). - - - Sei sicuro di volerti disconnettere? - - - Password Principale - Label for a master password. - - - Altro - Text to define that there are more options things to see. - - - Cassaforte - The title for the vault page. - - - Nome - Label for an entity name. - - - No - - - Note - Label for notes. - - - Ok - Acknowledgement. - - - Password - Label for a password. - - - Salva - Button text for a save operation (verb). - - - Salvataggio... - Message shown when interacting with the server - - - Impostazioni - The title for the settings page. - - - Mostra - Reveal a hidden value (password). - - - L'elemento è stato eliminato. - Confirmation message after successfully deleting a login. - - - Invia - - - Sincronizza - The title for the sync page. - - - Grazie - - - Strumenti - The title for the tools page. - - - URI - Label for a uri/url. - - - Usa l'impronta per sbloccare - - - Nome Utente - Label for a username. - - - Il campo {0} è richiesto. - Validation message for when a form field is left blank and is required to be entered. - - - {0} è stato copiato. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verifica Impronta - - - Verifica Password Principale - - - Verifica PIN - - - Versione - - - Visualizza - - - Visita il nostro Sito - - - Visita il nostro sito per ottenere aiuto, notizie, mandarci una email e/o imparare di più su come usare Bitwarden. - - - Sito - Label for a website. - - - - - - Account - - - Il tuo nuovo account è stato creato! Ora puoi fare il login. - - - Aggiungi un elemento - - - Estensione App - - - Usa il servizio di accessibilità Bitwarden per l'auto-completamento dei tuoi login su app e web. - - - Servizio di Auto-Riempimento - - - Evita Caratteri Ambigui - - - Estensione App Bitwarden - - - Il metodo più sempice per aggiungere nuovi login alla tua cassaforte è dall'Estensione App Bitwarden. Scopri di più su come utilizzarla nella schermata "Strumenti". - - - Usa Bitwarden su Safari e altre app per auto-completare i tuoi login. - - - Servizio Auto-Completamento Bitwarden - - - Usa il servizio di accessibilità Bitwarden per auto-completare i tuoi login. - - - Cambia Email - - - Puoi cambiare il tuo indirizzo email solo sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito? - - - Cambia Password Principale - - - Puoi cambiare la tua password principale solo sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito? - - - Chiudi - - - In Arrivo! - - - Continua - - - Copiato! - - - Password copiata! - - - Nome utente copiato! - - - Crea Account - - - Sto creando l'account... - Message shown when interacting with the server - - - Modifica elemento - - - Abilita la Sincronizzazione Automatica - - - Inserisci l'indirizzo email del tuo account per ricevere l'indizio della Password Principale. - - - Riabilita l'Estensione App - - - Quasi terminato! - - - Abilita l'Estensione App - - - Su Safari, trova Bitwarden utilizzando l'icona di condivisione (suggerimento: scorri a destra sulla riga inferiore del menu). - Safari is the name of apple's web browser - - - Ottieni accesso immediato alle tue password! - - - Sei pronto a fare il login! - - - Vedi Applicazioni Supportate - - - I tuoi login sono ora facilmente accessibile da Safari, Chrome e altre applicazioni supportate. - - - In Safari e Chrome, trova Bitwarden utilizzando l'icona di condivisione (suggerimento: scorri a destra sulla riga in basso del menu Condividi). - - - Tocca l'icona di Bitwarden nel menu per avviare l'estensione. - - - Per attivare Bitwarden su Safari e altre applicazioni, tocca l'icona "altro" sull'ultima riga del menu. - - - Preferito - - - Impronta - - - Genera Password - - - Ottenere il suggerimento per la password principale - - - Importa elementi - - - Puoi importare in massa gli elementi dal vault web bitwarden.com. Vuoi visitare ora il sito? - - - Importa rapidamente i tuoi elementi da altre app di gestione password. - - - Ultima Sincronizzazione: - - - Lunghezza - - - Blocca - - - 15 minuti - - - 1 ora - - - 1 minuto - - - 4 ore - - - Immediatamente - - - Opzioni di blocco - - - Connessione in corso... - Message shown when interacting with the server - - - Fai il log in o crea un nuovo account per accedere alla tua cassaforte di sicurezza. - - - Gestisci - - - La conferma della password non è corretta. - - - La password principale è la password che utilizzi per accedere alla tua cassaforte. È molto importante che tu non la dimentichi. Non c'è modo di recuperare questa password nel caso che tu la dimenticassi. - - - Indizio per la Password Principale (opzionale) - - - Un indizio per la password principale può aiutarti a ricordarla nel caso te la dimenticassi. - - - La password principale deve essere lunga almeno 8 caratteri. - - - Minimo di Numeri - Minimum numeric characters for password generator settings - - - Minimo di Speciali - Minimum special characters for password generator settings - - - Altre impostazioni - - - È necessario accedere all'app di Bitwarden principale prima di poter utilizzare l'estensione. - - - Mai - - - Nuovo elemento creato. - - - Non ci sono preferiti nella tua cassaforte. - - - Non ci sono elementi nella tua cassaforte. - - - Non ci sono elementi nella tua cassaforte per questo sito web. Clicca per aggiungerne uno. - - - Questo login non dispone di un nome utente o password configurata. - - - Ok, ho capito! - Confirmation, like "Ok, I understand it" - - - Le opzioni predefinite sono impostate dal generatore di password dell'app principale Bitwarden. - - - Opzioni - - - Altro - - - Password generata. - - - Generatore di Password - - - Indizio Password - - - Ti abbiamo inviato una email col tuo indizio per la password principale. - - - Sei sicuro di voler sovrascrivere la password corrente? - - - Bitwarden mantiene la tua cassaforte automaticamente sincronizzata utilizzando le notifiche push. Per la migliore esperienza possibile, si prega di selezionare "Consenti" sul menu seguente quando viene richiesto di abilitare le notifiche push. - Push notifications for apple products - - - Valuta l'applicazione - - - Per favore prendi in considerazione l'idea di lasciarci una buona recensione! - - - I voti dell'App Store vengono reimpostati con ogni nuova versione di Bitwarden. Aiutaci con una buona recensione! - - - Rigenera Password - - - Ri-digita la Password Principale - - - Cerca nella cassaforte - - - Sicurezza - - - Vedi i progressi di sviluppo - - - Seleziona - - - Imposta PIN - - - Immetti un codice PIN a 4 cifre con cui sbloccare l'applicazione. - - - Informazioni elemento - - - Elemento aggiornato. - - - Invio in corso... - Message shown when interacting with the server - - - Sincronizzazione in corso... - Message shown when interacting with the server - - - Sincronizzazione completata. - - - Sincronizzazione fallita. - - - Sincronizza Ora La Tua Cassaforte - - - Touch ID - What Apple calls their fingerprint reader. - - - Verifica in due passaggi - - - L'autenticazione in due passaggi rende il tuo account più sicuro, richiedendo di verificare il login con un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata o email. Può essere abilitata su bitwarden.com. Vuoi visitare il sito ora? - - - Sblocca con {0} - - - Sblocca con codice PIN - - - Convalidando - Message shown when interacting with the server - - - Codice di Verifica - - - Visualizza Elemento - - - Cassaforte Web Bitwarden - - - Gestisci i tuoi login da qualunque browser con la cassaforte web Bitwarden. - - - Perso l'app di autenticazione? - - - Elementi - Screen title - - - Estensione attivata! - - - Icone - - - Traduzioni - - - Elementi per {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Non ci sono elementi nella tua cassaforte per {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Quando vedi una notifica di completamento automatico Bitwarden, puoi toccarla per avviare il servizio di completamento automatico. - - - Tocca questa notifica per auto-completare un login dal tuo vault. - - - Apri impostazioni d'accessibilità - - - 1. Nella schermata impostazioni di accessibilità di Android, tocca "Bitwarden". - - - 2. Attiva l'interruttore e premi OK per accettare. - - - Disattivo - - - Attivo - - - Stato - - - Beta - - - Il modo più semplice per aggiungere nuovi login al tuo vault è dal servizio di completamento automatico Bitwarden. Trova altre informazioni sull'utilizzo del servizio di auto-completamnto accedendo alla schermata "Strumenti". - - - Auto-completamento - - - Vuoi auto-completare o vedere questo login? - - - Sei sicuro di voler completare automaticamente questo login? Non si tratta di una corrispondenza completa per "{0}". - - - Elementi corrispondenti - - - Possibili elementi corrispondenti - - - Cerca - - - Stai cercando un login di completamento automatico per "{0}". - - - Condividi La Tua Cassaforte - - - Crea un'organizzazione per condividere in sicurezza i tuoi login con altri utenti. - - - Rileva quando un campo password è selezionato - - - Rileva solamente i campi da riempire e offri una notifica per il riempimento. Questa opzione potrebbe risparmiare batteria. - - - Notifiche persistenti - - - Offri sempre una notifica di auto-completamento e rileva campi solo dopo un completamento. Questa opzione potrebbe risparmiare batteria. - - - Rileva Sempre - - - Rileva sempre campi e offri notifica di auto-riempimento solo se vengono trovati campi di password. Questa è l'opzione di default. - - - Impossibile aprire l'app "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - App di Autenticazione - For 2FA - - - Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione. - For 2FA - - - Inserisci il codice di verifica a 6 cifre ricevuto per email su {0}. - For 2FA - - - Login non disponibile - For 2FA whenever there are no available providers on this device. - - - Questo account ha il login in due passaggi attivato, ma nessuno dei fornitori di autenticazione in due passaggi configurati è supportato su questo dispositivo. Per favore usa un dispositivo supportato e/o aggiungine uno meglio supportato sui tuoi dispositivi (come una app di autenticazione). - - - Codice di Recupero - For 2FA - - - Ricordami - Remember my two-step login - - - Invia nuovamente codice di verifica email - For 2FA - - - Opzioni di login in due passaggi - - - Usa un altro metodo di login in due passaggi - - - Impossibile inviare codice di verifica email. Riprova. - For 2FA - - - Email di verifica inviata. - For 2FA - - - Per continuare, apoggia la tua YubiKey NEO sul retro del dispositivo o inserisci la tua Yubikey nell porta USB, poi tocca il suo bottone. - - - Chiave di Sicurezza YubiKey - "YubiKey" is the product name and should not be translated. - - - Aggiungi Nuovo Allegato - - - Allegati - - - Impossibile scaricare il file. - - - Il tuo dispositivo non può aprire questo tipo di file. - - - Download in corso... - Message shown when downloading a file - - - Questo allegato ha dimensione {0}. Sei sicuro di volerlo scaricare sul tuo dipositivo? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Chiave di Autenticazione (TOTP) - - - Codice di Verifica (TOTP) - Totp code label - - - Chiave di autenticazione aggiunta. - - - Impossibile leggere chiave di autenticazione. - - - La scannerizzazione avverrà automaticamente. - - - Punta la fotocamera sul codice QR. - - - Scansione Codice QR - - - Fotocamera - - - Foto - - - TOTP Copiato! - - - Copia TOTP - - - Se il tuo login ha un autenticatore collegato, il codice ti verifica TOTP è automaticamente copiato negli appunti ogni volta che auto-completi il login. - - - Disattiva Copia TOTP Automatica - - - Un abbonamento premium è richiesto per utilizzare questa funzionalità. - - - Allegato aggiunto - - - Allegato eliminato - - - Seleziona File - - - File - - - Nessun file selezionato - - - Non ci sono allegati. - - - Seleziona File - - - Funzione Non Disponibile - - - La dimensione massima del file è 100 MB. - - - Non puoi utilizzare questa funzione finchè non aggiorni la tua chiave di crittografia. - - - Altre Informazioni - - - URL del Server API - - - Ambiente Personalizzato - - - Per utenti avanzati. Puoi specificare l'URL principale di ogni servizio indipendentemente. - - - Gli URL dell'ambiente sono stati salvati. - - - {0} non è formattato correttamente. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL del Server di Identità - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Ambiente Self-Hosted - - - Specifica l'URL principale della tua installazione bitwarden self-hosted. - - - URL sel Server - - - URL della Cassaforte Online - - - Premi us questa notifica per visualizzare elementi dalla tua cassaforte. - - - Campi Personalizzati - - - Copia numero - - - Copia codice di sicurezza - - - Numero - - - Codice di sicurezza - - - Che tipo di elemento vuoi aggiungere? - - - Carta - - - Identità - - - Accesso - - - Nota sicura - - - Indirizzo 1 - - - Indirizzo 2 - - - Indirizzo 3 - - - Aprile - - - Agosto - - - Marca - - - Titolare Carta - - - Città / Comune - - - Azienda - - - Nazione - - - Dicembre - - - Dr - - - Mese di scadenza - - - Anno di scadenza - - - Febbraio - - - Nome - - - Gennaio - - - Luglio - - - Giugno - - - Cognome - - - Numero patente - - - Marzo - - - Maggio - - - Secondo Nome - - - Sig - - - Sig. ri - - - Sig. ra - - - Novembre - - - Ottobre - - - Numero di passaporto - - - Telefono - - - Settembre - - - Codice Fiscale - - - Stato / Provincia - - - Titolo - - - CAP - - - Indirizzo - - - Scadenza - - - Disabilita icone Siti Web - - - Le icone del sito web fornisce un'immagine riconoscibile accanto ad ogni elemento di accesso nella tua cassaforte. - - - URL del Server Icone - - - Auto-completamento con Bitwarden - - - La cassaforte è bloccata - - - Vai alla mia cassaforte - - - Collezioni - - - Non ci sono elementi in questa collezione. - - - Non ci sono elementi in questa cartella. - - - Servizio di accessibilità dell'auto riempimento - - - Il servizio di auto-completamento Bitwarden usa l'Android Autofill Framework per assistenza nel completare informazioni su login, carte di credito e identità su altre app nel tuo dispositivo. - - - Usa il servizio di auto-completamento Bitwarden per riempire le informazioni su login, carte di credito e identità su altre app. - - - Apri impostazioni di auto-riempimento - - - Face ID - What Apple calls their facial recognition reader. - - - Usa Face ID per verificare. - - - Usa Face ID per sbloccare - - - Verifica Face ID - - - Sblocca con Windows Hello - - - Verifica con Windows Hello - - - Windows Hello - - - Non è stato possibile aprire il menu di impostazioni di auto-riempimento per te. Puoi navigare manualmente al menu delle impostazioni di auto-riempimento dalle Impostazioni Android > Accessibilità > Servizi > Bitwarden. - - - Nome campo personalizzato - - - Booleano - - - Nascosto - - - Testo - - - Nuovo campo personalizzato - - - Che tipo di campo personalizzato vuoi aggiungere? - - - Rimuovi - - - Nuovo URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Dominio di base - - - Predefinito - - - Esatto - - - Host - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Espressione regolare - A programming term, also known as 'RegEx'. - - - Inizia con - - - Rilevamento di corrispondenza URI - - - Rilevamento di corrispondenza - URI match detection for auto-fill. - - - Si, e Salva - - - Riempi automaticamente e salva - - - Organizzazione - An entity of multiple related people (ex. a team or business organization). - - - Tieni il tuo Yubikey vicino alla parte superiore del dispositivo. - - - Riprova - - - Per continuare, tieni il tuo YubiKey NEO sul retro del dispositivo. - - - Il servizio di accessibilità può essere utile quando le app non supportano il servizio di riempimento automatico standard. - - - Password Aggiornata - ex. Date this password was updated - - - Aggiornato - ex. Date this item was updated - - - Riempimento automatico Attivato! - - - È necessario accedere all'app principale di Bitwarden prima di poter utilizzare il riempimento automatico. - - - I tuoi login sono ora facilmente accessibili direttamente dalla tastiera durante l'accesso ad app e siti web. - - - Se non prevedi di utilizzarli, ti consigliamo di disabilitare qualsiasi altra applicazione di Compilazione automatica nelle Impostazioni. - - - Accedi al tuo caveau direttamente dalla tastiera per velocizzare il riempimento automatico delle password. - - - Per abilitare la compilazione automatica della password sul tuo dispositivo, segui queste istruzioni: - - - 1. Vai all'app "Impostazioni" di iOS - - - 2. Clicca su "Password & Account" - - - 3. Clicca su "Riempimento automatico password" - - - 4. Attiva Riempimento Automatico - - - 5. Seleziona "Bitwarden" - - - Riempimento Automatico Password - - - Il modo più semplice per aggiungere nuovi accessi al tuo caveau è utilizzare l'estensione di Compilazione automatica password di Bitwarden. Ulteriori informazioni sull'utilizzo dell'estensione di Compilazione automatica password di Bitwarden andando nella schermata "Strumenti". - - diff --git a/src/App/Resources/AppResources.ja.Designer.cs b/src/App/Resources/AppResources.ja.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.ja.resx b/src/App/Resources/AppResources.ja.resx deleted file mode 100644 index 2b61fad8f..000000000 --- a/src/App/Resources/AppResources.ja.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - アプリ情報 - - - 追加 - Add/create a new entity (verb). - - - フォルダーを追加 - - - アイテムの追加 - The title for the add item page. - - - エラーが発生しました。 - Alert title when something goes wrong. - - - 戻る - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - キャンセル - Cancel an operation. - - - コピー - Copy some value to your clipboard. - - - パスワードをコピー - The button text that allows a user to copy the login's password to their clipboard. - - - ユーザー名をコピー - The button text that allows a user to copy the login's username to their clipboard. - - - クレジット - Title for page that we use to give credit to resources that we use. - - - 削除 - Delete an entity (verb). - - - 削除中… - Message shown when interacting with the server - - - 本当に削除しますか?この操作は元に戻せません。 - Confirmation alert message when deleteing something. - - - 編集 - - - フォルダーを編集 - - - メール - Short label for an email address. - - - メールアドレス - Full label for a email address. - - - メールでのお問い合わせ - - - メールで直接ヘルプやフィードバックを送れます。 - - - PIN コードを入力してください。 - - - お気に入り - Title for your favorite items in the vault. - - - バグレポートを送信 - - - GitHub リポジトリで Issue を作成してください。 - - - 指紋で認証します。 - - - フォルダー - Label for a folder. - - - 新しいフォルダーを作成しました。 - - - フォルダーを削除しました。 - - - フォルダーなし - Items that have no folder specified go in this special "catch-all" folder. - - - フォルダー - - - フォルダーを更新しました。 - - - ウェブサイトを開く - The button text that allows user to launch the website to their web browser. - - - ヘルプとフィードバック - - - 隠す - Hide a secret value that is currently shown (password). - - - 続行する前に、インターネットに接続してください。 - Description message for the alert when internet connection is required to continue. - - - インターネット接続が必要です - Title for the alert when internet connection is required to continue. - - - マスターパスワードが間違っています。入力し直してください。 - - - PIN が間違っています。入力し直してください。 - - - 開く - The button text that allows user to launch the website to their web browser. - - - ログイン - The login button text (verb). - - - ログイン - Title for login page. (noun) - - - ログアウト - The log out button text (verb). - - - ログアウトしてもよろしいですか? - - - マスターパスワード - Label for a master password. - - - 詳細 - Text to define that there are more options things to see. - - - 保管庫 - The title for the vault page. - - - 名前 - Label for an entity name. - - - いいえ - - - メモ - Label for notes. - - - OK - Acknowledgement. - - - パスワード - Label for a password. - - - 保存 - Button text for a save operation (verb). - - - 保存中... - Message shown when interacting with the server - - - 設定 - The title for the settings page. - - - 表示 - Reveal a hidden value (password). - - - アイテムを削除しました。 - Confirmation message after successfully deleting a login. - - - 送信 - - - 同期 - The title for the sync page. - - - ありがとうございます - - - ツール - The title for the tools page. - - - URI - Label for a uri/url. - - - 指紋認証でロック解除 - - - ユーザー名 - Label for a username. - - - {0} は必須です。 - Validation message for when a form field is left blank and is required to be entered. - - - {0} をコピーしました。 - Confirmation message after suceessfully copying a value to the clipboard. - - - 指紋の確認 - - - マスターパスワードの確認 - - - PIN の確認 - - - バージョン - - - 表示 - - - ウェブサイトを開く - - - 私たちのウェブサイトでヘルプ、ニュース、問い合わせや Bitwarden の詳しい使い方をチェックしてください。 - - - ウェブサイト - Label for a website. - - - はい - - - アカウント - - - 新しいアカウントを作成しました!今すぐログインできます。 - - - アイテムの追加 - - - App Extension - - - Bitwarden ユーザー補助サービスを使ってアプリやウェブサイトでログイン情報を自動入力します。 - - - 自動入力サービス - - - あいまいな文字を省く - - - Bitwarden App Extension - - - Bitwarden App Extension から新しいログイン情報を保管庫に追加するのが一番簡単です。ツール画面で App Extension の詳細を確認できます。 - - - Safari などのアプリで Bitwarden を使ってログイン情報を自動入力します。 - - - Bitwarden 自動入力サービス - - - Bitwarden ユーザー補助サービスを使ってログイン情報を自動入力します。 - - - メールアドレスの変更 - - - メールアドレスは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか? - - - マスターパスワードの変更 - - - マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか? - - - 閉じる - - - 近日公開! - - - 続行 - - - コピーしました! - - - パスワードをコピーしました! - - - ユーザー名をコピーしました! - - - アカウントの作成 - - - アカウントの作成中... - Message shown when interacting with the server - - - アイテムの編集 - - - 自動同期を有効化 - - - マスターパスワードのヒントを受信するため、アカウントのメールアドレスを入力してください。 - - - アプリの拡張機能を再度有効化 - - - ほぼ完了! - - - アプリの拡張機能を有効化 - - - Safari では、共有アイコンを使って Bitwarden を見つけてください(ヒント:メニューの一番下の行を右側にスクロール) - Safari is the name of apple's web browser - - - パスワードに素早くアクセスしましょう! - - - ログインする準備が出来ました! - - - 対応アプリを表示 - - - Safari やChrome などの対応アプリでログイン情報に簡単にアクセスできます。 - - - Safari と Chrome では、共有アイコンを使って Bitwarden を見つけてください(ヒント:メニューの一番下の行を右側にスクロール) - - - メニューの Bitwarden アイコンをタップして拡張機能を起動してください。 - - - Bitwarden を Safari などのアプリで開くには、メニューの一番下の行の「詳細」をタップしてください。 - - - お気に入り - - - 指紋認証 - - - パスワードの自動生成 - - - マスターパスワードのヒントを取得 - - - アイテムのインポート - - - Bitwarden.com ウェブ保管庫から一括でインポートできます。ウェブサイトを開きますか? - - - 他のパスワード管理アプリからアイテムを一括インポートします。 - - - 前回の同期: - - - 長さ - - - ロック - - - 15分 - - - 1時間 - - - 1分 - - - 4時間 - - - すぐに - - - ロックオプション - - - ログイン中... - Message shown when interacting with the server - - - 安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。 - - - 管理 - - - パスワードの確認が正しくありません。 - - - マスターパスワードは、パスワード保管庫へのアクセスに使用するパスワードです。マスターパスワードを忘れないように注意してください。忘れた場合、パスワードを回復する方法はありません。 - - - マスターパスワードのヒント (省略可能) - - - マスターパスワードのヒントは、パスワードを忘れた場合に役立ちます。 - - - マスターパスワードは、少なくとも8文字以上で設定してください。 - - - 数字の最小数 - Minimum numeric characters for password generator settings - - - 記号の最小数 - Minimum special characters for password generator settings - - - その他の設定 - - - この拡張機能を使用する前に、メインの Bitwarden アプリにログインしてください。 - - - なし - - - 新しいアイテムを作成しました。 - - - 保管庫にお気に入りはありません。 - - - 保管庫にアイテムはありません。 - - - 保管庫にこのウェブサイトのアイテムはありません。タップで追加します。 - - - このログイン情報はユーザー名やパスワードが設定されていません。 - - - 分かりました! - Confirmation, like "Ok, I understand it" - - - デフォルトのオプションはメインの Bitwarden アプリのパスワード生成ツールから設定されます。 - - - オプション - - - その他 - - - パスワードを生成しました。 - - - パスワード生成ツール - - - パスワードのヒント - - - マスターパスワードのヒントを記載したメールを送信しました。 - - - 現在のパスワードを上書きしてよろしいですか? - - - Bitwarden はプッシュ通知を使って自動的に保管庫を同期します。プッシュ通知を有効化するかどうかプロンプトが出たら「許可する」を押してください。 - Push notifications for apple products - - - アプリを評価 - - - 良いレビューで私たちを助けてください! - - - App Store は Bitwarden の新しいバージョンが公開される毎に評価をリセットします。良いレビューで私たちを助けてください! - - - パスワードの再生成 - - - マスターパスワードを再入力 - - - 保管庫を検索 - - - セキュリティ - - - 開発状況を表示 - - - 選択 - - - PIN を設定 - - - アプリのロック解除に使う4桁の PIN コードを入力してください。 - - - アイテム情報 - - - アイテムを更新しました。 - - - 送信中... - Message shown when interacting with the server - - - 同期中... - Message shown when interacting with the server - - - 同期が完了しました。 - - - 同期に失敗しました。 - - - 保管庫を同期する - - - Touch ID - What Apple calls their fingerprint reader. - - - 2段階認証 - - - 2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は Bitwarden ウェブ保管庫で有効化できます。ウェブサイトを開きますか? - - - {0}でロック解除 - - - PIN コードでロック解除 - - - 認証中 - Message shown when interacting with the server - - - 認証コード - - - アイテムの表示 - - - Bitwarden ウェブ保管庫 - - - Bitwarden ウェブ保管庫であらゆるブラウザのログイン情報を管理します。 - - - 認証アプリを無くしてしまった場合 - - - アイテム - Screen title - - - 拡張機能を有効化しました! - - - アイコン - - - 翻訳 - - - {0}用のアイテム - This is used for the autofill service. ex. "Logins for twitter.com" - - - 保管庫に{0}用のアイテムはありません。 - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Bitwarden 自動入力通知が表示されたら、タップして自動入力サービスを起動できます。 - - - この通知をタップすると保管庫から自動入力します。 - - - ユーザー補助設定を開く - - - 1. Android のユーザー補助設定で「Bitwarden」をタッチしてください。 - - - 2. トグルをオンにして、「OK」を押してください。 - - - 無効 - - - 有効 - - - 状態 - - - ベータ - - - Bitwarden 自動入力サービスから新しいログイン情報を保管庫に追加するのが一番簡単です。ツール画面で自動入力サービスの詳細を確認できます。 - - - 自動入力 - - - このアイテムを自動入力するか表示しますか? - - - このアイテムを自動入力しますか? 「{0}」とは完全には一致していません。 - - - 一致するアイテム - - - 一致するかも知れないアイテム - - - 検索 - - - 「{0}」への自動入力アイテムを検索中です。 - - - 保管庫の共有 - - - 他のユーザーとアイテムを安全に共有する組織を作成します。 - - - パスワードフィールドのフォーカス時にスキャン - - - パスワードフィールドを選択したときだけフィールドをスキャンして、自動入力通知を表示します。バッテリー消費を抑えられるかもしれません。 - - - 通知を常に表示 - - - 自動入力通知を常に表示して、自動入力しようとした時だけフィールドをスキャンします。バッテリー消費を抑えられるかもしれません。 - - - 常にスキャン - - - 常に画面をスキャンして、パスワードフィールドが見つかった時だけ自動入力通知を表示します。これがデフォルト設定です。 - - - {0} を開けません。 - Message shown when trying to launch an app that does not exist on the user's device. - - - 認証アプリ - For 2FA - - - 認証アプリに表示された6桁の認証コードを入力してください。 - For 2FA - - - {0} に送信された6桁の認証コードを入力してください。 - For 2FA - - - ログインができません。 - For 2FA whenever there are no available providers on this device. - - - このアカウントは2段階認証が有効ですが、この端末に対応した認証プロパイダが一つも設定されていません。対応端末を使用するか、より幅広い端末に対応した認証プロパイダを追加してください。 - - - リカバリーコード - For 2FA - - - 情報を保存する - Remember my two-step login - - - 確認コードをメールで再送 - For 2FA - - - 2段階認証オプション - - - 他の2段階認証方法を使用 - - - 確認メールを送信できませんでした。やり直してください。 - For 2FA - - - 確認メールを送信しました。 - For 2FA - - - 端末の NFC ポートに YubiKey NEO を近づけたまま続行してください。 - - - YubiKey セキュリティキー - "YubiKey" is the product name and should not be translated. - - - 添付ファイルを追加 - - - 添付ファイル - - - ファイルをダウンロードできません。 - - - お使いの端末ではこの種類のファイルを開けません。 - - - ダウンロード中... - Message shown when downloading a file - - - この添付ファイルのサイズは{0}です。本当に端末にダウンロードしますか? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - 認証キー (TOTP) - - - 認証コード (TOTP) - Totp code label - - - 認証キーを追加しました。 - - - 認証キーを読み取れません。 - - - 自動的に読み取ります。 - - - カメラで QR コードを写してください。 - - - QR コードを読み取る - - - カメラ - - - 写真 - - - TOTP をコピーしました! - - - TOTP のコピー - - - ログイン情報に認証キーが添付されている場合、自動入力した時に認証コードが自動的にクリップボードへコピーされます。 - - - 認証コードの自動コピーを無効化 - - - この機能を使うにはプレミアム会員になってください。 - - - 添付ファイルを追加しました - - - 添付ファイルを削除しました - - - ファイルを選択 - - - ファイル - - - ファイルが選択されていません - - - 添付ファイルがありません。 - - - ファイルソース - - - サービスが利用できません - - - 最大ファイルサイズは100MBです。 - - - 暗号キーを更新するまでこの機能は使用できません。 - - - 詳細情報 - - - API サーバー URL - - - カスタム環境 - - - 上級者向けです。各サービスのベース URL を個別に指定できます。 - - - 環境 URL を保存しました。 - - - {0} の形式が正しくありません。 - Validation error when something is not formatted correctly, such as a URL or email address. - - - ID サーバー URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - セルフホスティング環境 - - - セルフホスティングしている Bitwarden のベース URL を指定してください。 - - - サーバー URL - - - ウェブ保管庫サーバー URL - - - この通知をタップすると保管庫のアイテムを表示します。 - - - カスタムフィールド - - - 番号のコピー - - - セキュリティコードのコピー - - - 番号 - - - セキュリティコード - - - どのタイプのアイテムを追加しますか? - - - カード - - - ID - - - ログイン - - - セキュアメモ - - - 住所1 - - - 住所2 - - - 住所3 - - - 4月 - - - 8月 - - - ブランド - - - カードの名義人名 - - - 市町村 - - - 会社名 - - - - - - 12月 - - - Dr - - - 有効期限月 - - - 有効期限年 - - - 2月 - - - - - - 1月 - - - 7月 - - - 6月 - - - - - - 免許証番号 - - - 3月 - - - 5月 - - - ミドルネーム - - - Mr - - - Mrs - - - Ms - - - 11月 - - - 10月 - - - パスポート番号 - - - 電話番号 - - - 9月 - - - 社会保障番号 - - - 都道府県 - - - 敬称 - - - 郵便番号 - - - 住所 - - - 有効期限 - - - ウェブサイトアイコンの無効化 - - - 保管庫のアイテム毎にウェブサイトのアイコンを表示します。 - - - アイコンのサーバー URL - - - Bitwarden で自動入力 - - - 保管庫はロックされています - - - 保管庫を表示 - - - コレクション - - - このコレクションにはアイテムがありません。 - - - このフォルダーにはアイテムがありません。 - - - 自動入力ユーザー補助サービス - - - Bitwarden 自動入力サービスは Android の Autofill Framework を使ってログイン情報やクレジットカード、ID情報をお使いのアプリに入力するのを補助します。 - - - Bitwarden 自動入力サービスを使ってログイン情報やクレジットカード、ID情報などをアプリで自動入力します。 - - - 自動入力設定を開く - - - Face ID - What Apple calls their facial recognition reader. - - - Face ID を使用して認証します。 - - - Face ID を使用してロック解除 - - - Face ID で認証 - - - Windows Hello でロック解除 - - - Windows Hello で認証 - - - Windows Hello - - - Android の自動入力設定を自動的に開けませんでした。Android の設定 > システム > 言語と入力 > 詳細設定 > 自動入力サービスで自動入力を設定できます。 - - - カスタムフィールド名 - - - 真偽値 - - - 非表示 - - - テキスト - - - 新規カスタムフィールド - - - どのタイプのカスタムフィールドを追加しますか? - - - 削除 - - - 新しい URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - ベースドメイン - - - デフォルト - - - 完全一致 - - - ホスト - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - 正規表現 - A programming term, also known as 'RegEx'. - - - 前方一致 - - - URI の一致検出方法 - - - 一致検出方法 - URI match detection for auto-fill. - - - 保存する - - - 自動入力して保存 - - - 組織 - An entity of multiple related people (ex. a team or business organization). - - - YubiKey を端末の上部にしっかりタッチし続けてください。 - - - もう一度実行 - - - 続行するには、YubiKey NEO を端末の背面にしっかりタッチし続けてください。 - - - ユーザー補助サービスはアプリが標準の自動入力サービスに対応していない場合に適しています。 - - - パスワード更新日 - ex. Date this password was updated - - - 更新日 - ex. Date this item was updated - - - 自動入力を有効化しました! - - - 自動入力を使うには、まず Bitwarden アプリでログインしてください。 - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - 使う予定が無い場合は、設定で他の自動入力アプリを無効化することをおすすめします。 - - - キーボードから直接保管庫にアクセスしして、素早くパスワードを自動入力できます。 - - - パスワードの自動入力を有効化するには、下記の手順に従ってください: - - - 1. iOS の「設定」アプリを開く - - - 2. 「アカウントとパスワード」をタップ - - - 3. 「パスワードの自動入力」をタップ - - - 4. 自動入力を有効化 - - - 5. 「Bitwarden」を選択 - - - パスワード自動入力 - - - Bitwarden パスワード自動入力拡張機能を使うと、簡単に新しいログイン情報を保管庫に追加できます。「ツール」画面で詳しい使い方をご覧ください。 - - diff --git a/src/App/Resources/AppResources.ko.resx b/src/App/Resources/AppResources.ko.resx deleted file mode 100644 index fb3b28dc3..000000000 --- a/src/App/Resources/AppResources.ko.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 정보 - - - 추가 - Add/create a new entity (verb). - - - 폴더 추가 - - - 항목 추가 - The title for the add item page. - - - 오류가 발생했습니다. - Alert title when something goes wrong. - - - 뒤로 - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - 취소 - Cancel an operation. - - - 복사 - Copy some value to your clipboard. - - - 비밀번호 복사 - The button text that allows a user to copy the login's password to their clipboard. - - - 사용자 이름 복사 - The button text that allows a user to copy the login's username to their clipboard. - - - 크레딧 - Title for page that we use to give credit to resources that we use. - - - 삭제 - Delete an entity (verb). - - - 삭제 중... - Message shown when interacting with the server - - - 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - Confirmation alert message when deleteing something. - - - 편집 - - - 폴더 편집 - - - 이메일 - Short label for an email address. - - - 이메일 주소 - Full label for a email address. - - - 이메일 보내기 - - - 저희에게 이메일을 보내 도움을 요청하거나 의견을 말씀하세요. - - - PIN 코드를 입력하세요. - - - 즐겨찾기 - Title for your favorite items in the vault. - - - 버그 보고서 첨부 - - - GitHub 리포지터리에 Issue 티켓을 엽니다. - - - 지문을 사용하여 인증하세요. - - - 폴더 - Label for a folder. - - - 새 폴더를 만들었습니다. - - - 폴더를 삭제했습니다. - - - 폴더 없음 - Items that have no folder specified go in this special "catch-all" folder. - - - 폴더 - - - 폴더를 업데이트했습니다. - - - 웹 사이트로 이동 - The button text that allows user to launch the website to their web browser. - - - 도움말 및 의견 - - - 숨기기 - Hide a secret value that is currently shown (password). - - - 계속하기 전에 인터넷에 연결해 주세요. - Description message for the alert when internet connection is required to continue. - - - 인터넷 연결 필요 - Title for the alert when internet connection is required to continue. - - - 마스터 비밀번호가 잘못되었습니다. 다시 시도해 주세요. - - - PIN이 잘못되었습니다. 다시 시도해 주세요. - - - 열기 - The button text that allows user to launch the website to their web browser. - - - 로그인 - The login button text (verb). - - - 로그인 - Title for login page. (noun) - - - 로그아웃 - The log out button text (verb). - - - 정말 로그아웃하시겠습니까? - - - 마스터 비밀번호 - Label for a master password. - - - 자세히 - Text to define that there are more options things to see. - - - 내 보관함 - The title for the vault page. - - - 이름 - Label for an entity name. - - - 아니오 - - - 메모 - Label for notes. - - - 확인 - Acknowledgement. - - - 비밀번호 - Label for a password. - - - 저장 - Button text for a save operation (verb). - - - 저장 중... - Message shown when interacting with the server - - - 설정 - The title for the settings page. - - - 표시 - Reveal a hidden value (password). - - - 항목을 삭제했습니다. - Confirmation message after successfully deleting a login. - - - 보내기 - - - 동기화 - The title for the sync page. - - - 감사합니다 - - - 도구 - The title for the tools page. - - - URI - Label for a uri/url. - - - 지문으로 잠금 해제 - - - 사용자 이름 - Label for a username. - - - {0} 필드는 반드시 입력해야 합니다. - Validation message for when a form field is left blank and is required to be entered. - - - {0} 항목을 복사했습니다. - Confirmation message after suceessfully copying a value to the clipboard. - - - 지문 확인 - - - 마스터 비밀번호 확인 - - - PIN 확인 - - - 버전 - - - 보기 - - - 웹 사이트 방문 - - - 도움이 필요하거나 뉴스, 이메일 또는 Bitwarden을 사용하는 방법이 궁금하시면 웹 사이트를 방문하세요. - - - 웹 사이트 - Label for a website. - - - - - - 계정 - - - 계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다. - - - 항목 추가 - - - 앱 확장 프로그램 - - - 웹 사이트 및 앱에서 자동 완성 서비스를 사용하려면 Bitwarden 접근성 서비스를 사용 설정하세요. - - - 자동 완성 서비스 - - - 모호한 문자 사용 안 함 - - - Bitwarden 앱 확장 프로그램 - - - 보관함에 새 설정을 추가하는 가장 쉬운 방법은 Bitwarden 앱 확장 프로그램을 사용하는 것입니다. "도구" 화면으로 이동하여 Bitwarden 앱 확장 프로그램에 대해 더 자세히 알아보세요. - - - Safari 및 다른 앱에서 Bitwarden을 사용하여 로그인을 자동 완성하세요. - - - Bitwarden 자동 완성 서비스 - - - 자동 완성 서비스를 사용하려면 Bitwarden 접근성 서비스를 사용 설정하세요. - - - 이메일 변경 - - - bitwarden.com 웹 보관함에서 이메일 주소를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까? - - - 마스터 비밀번호 변경 - - - bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까? - - - 닫기 - - - 곧 출시됩니다! - - - 계속 - - - 복사했습니다! - - - 비밀번호를 복사했습니다! - - - 사용자 이름을 복사했습니다! - - - 계정 만들기 - - - 계정 생성 중... - Message shown when interacting with the server - - - 항목 편집 - - - 자동 동기화 사용 - - - 마스터 비밀번호 힌트를 받으려면 계정의 이메일 주소를 입력하세요. - - - 앱 확장 프로그램 재활성화 - - - 거의 다 됐습니다! - - - 앱 확장 프로그램 활성화 - - - Safari에서는 공유 아이콘을 사용하면 Bitwarden을 찾을 수 있습니다(참고: 메뉴의 아래쪽 줄을 오른쪽으로 스크롤하세요). - Safari is the name of apple's web browser - - - 비밀번호에 바로 접근하세요! - - - 로그인할 준비가 되었습니다! - - - 지원하는 앱 보기 - - - 이제 Safari, Chrome 및 다른 앱에서 로그인에 간단히 접근할 수 있습니다. - - - Safari 및 Chrome에서는 공유 아이콘을 사용하면 Bitwarden을 찾을 수 있습니다(참고: 공유 메뉴의 아래쪽 줄을 오른쪽으로 스크롤하세요). - - - 확장 프로그램을 실행하려면 메뉴에서 Bitwarden 아이콘을 누르세요. - - - Safari 및 다른 앱에서 Bitwarden을 사용 설정하려면 메뉴의 아래쪽 줄에서 "기타" 아이콘을 누르세요. - - - 즐겨찾기 - - - 지문 - - - 비밀번호 생성 - - - 마스터 비밀번호 힌트 얻기 - - - 항목 가져오기 - - - bitwarden.com 웹 보관함에서 항목을 한꺼번에 가져올 수 있습니다. 지금 웹 사이트를 방문하시겠습니까? - - - 다른 비밀번호 관리 앱에서 사용자의 항목을 한꺼번에 빠르게 가져옵니다. - - - 마지막 동기화: - - - 길이 - - - 잠금 - - - 15분 - - - 1시간 - - - 1분 - - - 4시간 - - - 즉시 - - - 잠금 옵션 - - - 로그인 중... - Message shown when interacting with the server - - - 안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요. - - - 관리 - - - 비밀번호 확인과 비밀번호가 일치하지 않습니다. - - - 마스터 비밀번호는 보관함을 열 때 필요한 비밀번호입니다. 절대 마스터 비밀번호를 잊어버리지 마세요. 잊어버리면 복구할 수 있는 방법이 없습니다. - - - 마스터 비밀번호 힌트 (선택) - - - 마스터 비밀번호 힌트는 마스터 비밀번호를 잊었을 때 도움이 될 수 있습니다. - - - 마스터 비밀번호는 최소 8자 이상이어야 합니다. - - - 숫자 최소 개수 - Minimum numeric characters for password generator settings - - - 특수 문자 최소 개수 - Minimum special characters for password generator settings - - - 기타 설정 - - - 확장 프로그램을 사용하려면 먼저 Bitwarden 앱에 로그인해야 합니다. - - - 잠그지 않음 - - - 새 항목을 만들었습니다. - - - 보관함에 추가한 즐겨찾기가 없습니다. - - - 보관함에 추가한 항목이 없습니다. - - - 보관함에 이 웹사이트에 대한 항목이 없습니다. 추가하려면 누르세요. - - - 이 로그인에는 사용자 이름 또는 비밀번호가 등록되어 있지 않습니다. - - - 네, 알겠습니다! - Confirmation, like "Ok, I understand it" - - - 옵션 기본값은 Bitwarden 앱의 비밀번호 생성기에서 설정할 수 있습니다. - - - 옵션 - - - 기타 - - - 비밀번호가 생성되었습니다. - - - 비밀번호 생성기 - - - 비밀번호 힌트 - - - 마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다. - - - 정말 현재 비밀번호를 덮어쓰시겠습니까? - - - Bitwarden은 푸시 알림을 사용하여 사용자의 보관함을 자동으로 동기화합니다. 다음 대화 상자에서 푸시 알림에 대해 물어보면 최상의 사용자 경험을 위해 "승인"을 선택해 주세요. - Push notifications for apple products - - - 앱 평가하기 - - - 좋은 리뷰를 남겨 저희를 도와주세요! - - - App Store의 평점은 Bitwarden의 새 버전이 나올 때마다 초기화됩니다. 좋은 리뷰를 남겨 저희를 도와주세요! - - - 비밀번호 재생성 - - - 마스터 비밀번호 다시 입력 - - - 보관함 검색 - - - 보안 - - - 개발 상황 보기 - - - 선택 - - - PIN 설정 - - - 앱 잠금을 해제할 4자리의 PIN 코드를 입력하세요. - - - 항목 정보 - - - 항목을 업데이트했습니다. - - - 전송 중... - Message shown when interacting with the server - - - 동기화 중... - Message shown when interacting with the server - - - 동기화를 완료했습니다. - - - 동기화에 실패했습니다. - - - 지금 보관함 동기화 - - - Touch ID - What Apple calls their fingerprint reader. - - - 2단계 인증 - - - 2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까? - - - {0}을 사용하여 잠금 해제 - - - PIN 코드로 잠금 해제 - - - 유효성 검증 중 - Message shown when interacting with the server - - - 인증 코드 - - - 항목 보기 - - - Bitwarden 웹 보관함 - - - Bitwarden 웹 보관함을 사용하여 다양한 종류의 브라우저에서 항목을 관리합니다. - - - 인증 앱에 접근할 수 없는 상황인가요? - - - 항목 - Screen title - - - 확장 프로그램이 활성화되었습니다! - - - 아이콘 - - - 번역 - - - {0}에 대한 항목 - This is used for the autofill service. ex. "Logins for twitter.com" - - - 보관함에 {0}에 대한 항목이 없습니다. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Bitwarden 자동 완성 알림을 확인한 경우, 알림을 눌러 자동 완성 서비스를 열 수 있습니다. - - - 보관함에 있는 항목으로 자동 완성을 사용하려면 이 알림을 누르세요. - - - 접근성 설정 열기 - - - 1. Android 접근성 설정 화면에서 "Bitwarden" 항목을 누르세요. - - - 2. 토글을 눌러 사용 설정하고 확인을 누르세요. - - - 꺼짐 - - - 켜짐 - - - 상태 - - - 베타 - - - 보관함에 새 설정을 추가하는 가장 쉬운 방법은 Bitwarden 자동 완성 서비스를 사용하는 것입니다. "도구" 화면으로 이동하여 Bitwarden 자동 완성 서비스에 대해 더 자세히 알아보세요. - - - 자동 완성 - - - 이 항목을 보거나 자동 완성하시겠습니까? - - - 정말 이 항목을 자동 완성하시겠습니까? "{0}"에 완벽히 일치하는 항목이 아닙니다. - - - 일치하는 항목 - - - 일치할 수도 있는 항목 - - - 검색 - - - "{0}"에 대한 자동 완성 항목을 보고 계십니다. - - - 내 보관함 공유 - - - 조직을 만들어 당신의 항목을 다른 사용자들과 안전하게 공유하세요. - - - 비밀번호 필드 선택 시 스캔 - - - 사용자가 비밀번호 필드를 선택했을 때만 화면상의 필드를 스캔하고 자동 완성 알림을 띄웁니다. 이 설정을 사용하면 배터리 소모를 줄일 수 있습니다. - - - 지속 알림 - - - 항상 자동 완성 알림을 띄웁니다. 그리고 자동 완성을 시도할 때만 필드를 스캔합니다. 이 설정을 사용하면 배터리 소모를 줄일 수 있습니다. - - - 항상 스캔 - - - 항상 화면상의 필드를 스캔합니다. 그리고 비밀번호 필드를 찾았을 때만 자동 완성 알림을 띄웁니다. 이 설정은 기본값입니다. - - - "{0}" 앱을 열 수 없습니다. - Message shown when trying to launch an app that does not exist on the user's device. - - - 인증 앱 - For 2FA - - - 인증 앱에서 6자리 인증 코드를 입력하세요. - For 2FA - - - {0} 주소로 전송된 6자리 인증 코드를 입력하세요. - For 2FA - - - 로그인 불가능 - For 2FA whenever there are no available providers on this device. - - - 이 계정은 2단계 인증을 사용합니다. 그러나 설정된 2단계 인증 중 이 기기에서 지원하는 방식이 없습니다. 지원하는 기기를 사용하거나 더 많은 기기를 지원하는 2단계 인증 방식(인증 앱 등)을 추가하세요. - - - 복구 코드 - For 2FA - - - 기억하기 - Remember my two-step login - - - 인증 코드 이메일 다시 보내기 - For 2FA - - - 2단계 인증 옵션 - - - 다른 2단계 인증 사용 - - - 인증 이메일을 보내지 못했습니다. 다시 시도하세요. - For 2FA - - - 인증 메일을 보냈습니다. - For 2FA - - - 계속하려면 YubiKey NEO를 기기의 뒤편에 대거나 YubiKey를 기기의 USB 포트에 삽입한 뒤, YubiKey의 버튼을 누르세요. - - - YubiKey 보안 키 - "YubiKey" is the product name and should not be translated. - - - 새 첨부 파일 추가 - - - 첨부 파일 - - - 파일을 다운로드할 수 없습니다. - - - 이 기기에서 열 수 없는 형식의 파일입니다. - - - 다운로드 중... - Message shown when downloading a file - - - 이 첨부 파일의 크기는 {0}입니다. 정말 이 파일을 기기에 다운로드하시겠습니까? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - 인증 키 (TOTP) - - - 인증 코드 (TOTP) - Totp code label - - - 인증 키를 추가했습니다. - - - 인증 키를 읽을 수 없습니다. - - - QR 코드가 자동으로 스캔됩니다. - - - QR 코드에 카메라를 가져다 대세요. - - - QR 코드 스캔 - - - 카메라 - - - 사진 - - - TOTP를 복사했습니다! - - - TOTP 복사 - - - 로그인에 인증 키가 연결되어 있을 경우, 그 로그인을 자동 완성할 때마다 TOTP 인증 코드가 클립보드에 자동으로 복사됩니다. - - - TOTP 자동 복사 사용 안 함 - - - 이 기능을 사용하려면 프리미엄 멤버십이 필요합니다. - - - 첨부 파일 추가함 - - - 첨부 파일 삭제함 - - - 파일 선택 - - - 파일 - - - 선택한 파일 없음 - - - 첨부 파일이 없습니다. - - - 파일 위치 - - - 기능 사용할 수 없음 - - - 최대 파일 크기는 100MB입니다. - - - 이 기능을 사용하려면 암호화 키를 업데이트해야 합니다. - - - 더 알아보기 - - - API 서버 URL - - - 사용자 지정 환경 - - - 고급 사용자 전용 설정입니다. 각 서비스의 기본 URL을 개별적으로 지정할 수 있습니다. - - - 환경 URL 값을 저장했습니다. - - - {0} 값이 형식에 맞지 않습니다. - Validation error when something is not formatted correctly, such as a URL or email address. - - - ID 서버 URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - 자체 호스팅 환경 - - - 온-프레미스 Bitwarden이 호스팅되고 있는 서버의 기본 URL을 지정하세요. - - - 서버 URL - - - 웹 보관함 서버 URL - - - 보관함에 있는 항목을 보려면 이 알림을 누르세요. - - - 사용자 지정 필드 - - - 번호 복사 - - - 보안 코드 복사 - - - 번호 - - - 보안 코드 - - - 어떤 항목을 추가하시겠습니까? - - - 카드 - - - 신원 - - - 로그인 - - - 보안 메모 - - - 주소 1 - - - 주소 2 - - - 주소 3 - - - 4월 - - - 8월 - - - 브랜드 - - - 카드 소유자 이름 - - - 읍 / 면 / 동 - - - 회사 - - - 국가 - - - 12월 - - - Dr - - - 만료 월 - - - 만료 연도 - - - 2월 - - - 이름 - - - 1월 - - - 7월 - - - 6월 - - - - - - 면허 번호 - - - 3월 - - - 5월 - - - 가운데 이름 - - - Mr - - - Mrs - - - Ms - - - 11월 - - - 10월 - - - 여권 번호 - - - 전화번호 - - - 9월 - - - 주민등록번호 - - - 시 / 도 - - - 제목 - - - 우편번호 - - - 주소 - - - 만료 - - - 웹 사이트 아이콘 사용 안 함 - - - 웹 사이트 아이콘을 사용하면 보관함 각 항목 옆에 이미지를 보여줍니다. - - - 아이콘 서버 URL - - - Bitwarden으로 자동 완성 - - - 보관함이 잠겨 있음 - - - 내 보관함으로 이동 - - - 컬렉션 - - - 이 컬렉션에 항목이 없습니다. - - - 이 폴더에 항목이 없습니다. - - - 자동 완성 접근성 서비스 - - - Bitwarden 자동 완성 서비스는 기기의 다른 앱에서 로그인, 신용 카드 및 신원 정보를 입력하기 위해 Android 자동완성 프레임워크를 사용합니다. - - - 다른 앱에서 로그인, 신용 카드, 신원 정보를 자동으로 채우려면 Bitwarden 자동완성 서비스를 사용하세요. - - - 자동완성 설정 열기 - - - Face ID - What Apple calls their facial recognition reader. - - - Face ID를 사용하여 인증하세요. - - - Face ID를 사용하여 잠금 해제 - - - Face ID를 사용하여 인증 - - - Windows Hello를 사용하여 잠금 해제 - - - Windows Hello를 사용하여 인증 - - - Windows Hello - - - Android 자동 완성 설정 메뉴를 자동으로 열지 못했습니다. Android 설정 > 시스템 > 언어 및 입력 > 고급 > 자동완성 서비스 메뉴로 직접 이동하실 수 있습니다. - - - 사용자 지정 필드 이름 - - - 참 / 거짓 - - - 숨김 - - - 텍스트 - - - 새 사용자 지정 필드 - - - 어떤 유형의 사용자 지정 필드를 추가하시겠습니까? - - - 제거 - - - 새 URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - 기본 도메인 - - - 기본값 - - - 정확히 일치 - - - 호스트 - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - 정규 표현식 - A programming term, also known as 'RegEx'. - - - ...으로 시작 - - - URI 일치 인식 - - - 일치 인식 - URI match detection for auto-fill. - - - 예, 저장하겠습니다. - - - 자동 완성 및 저장 - - - 조직 - An entity of multiple related people (ex. a team or business organization). - - - 기기의 위쪽에 Yubikey를 가져다 대세요. - - - 다시 시도 - - - 계속하려면 기기의 뒷면에 YubiKey NEO를 가져다 대세요. - - - 접근성 서비스는 앱이 표준 자동 완성 서비스를 지원하지 않을 때 유용할 수 있습니다. - - - 비밀번호 업데이트됨 - ex. Date this password was updated - - - 업데이트됨 - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. 자동 완성을 켜세요. - - - 5. "Bitwarden"을 선택하세요. - - - 비밀번호 자동 완성 - - - 보관함에 새 로그인을 추가하는 가장 쉬운 방법은 Bitwarden 비밀번호 자동 완성 확장 프로그램을 사용하는 것입니다. "도구" 화면으로 이동하여 Bitwarden 비밀번호 자동 완성 확장 프로그램에 대해 더 자세히 알아보세요. - - diff --git a/src/App/Resources/AppResources.nb.resx b/src/App/Resources/AppResources.nb.resx deleted file mode 100644 index 73f98e909..000000000 --- a/src/App/Resources/AppResources.nb.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Om - - - Legg til - Add/create a new entity (verb). - - - Legg til en mappe - - - Legg til gjenstand - The title for the add item page. - - - En feil har oppstått. - Alert title when something goes wrong. - - - Tilbake - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Lukk - Cancel an operation. - - - Kopier - Copy some value to your clipboard. - - - Kopier passordet - The button text that allows a user to copy the login's password to their clipboard. - - - Kopier brukernavnet - The button text that allows a user to copy the login's username to their clipboard. - - - Krediteringer - Title for page that we use to give credit to resources that we use. - - - Slett - Delete an entity (verb). - - - Sletter... - Message shown when interacting with the server - - - Vil du virkelig slette den? Dette kan ikke angres på. - Confirmation alert message when deleteing something. - - - Rediger - - - Rediger mappen - - - E-post - Short label for an email address. - - - E-postadresse - Full label for a email address. - - - Send oss E-post - - - Send E-post direkte til oss for å få hjelp eller gi tilbakemeldinger. - - - Tast inn din PIN-kode. - - - Favoritter - Title for your favorite items in the vault. - - - Legg inn feilrapport - - - Åpne en sak (Issue) på vårt GitHub-lager. - - - Bruk ditt fingeravtrykk for å verifisere. - - - Mappe - Label for a folder. - - - En ny mappe er opprettet. - - - Mappen er slettet. - - - Ingen mappe - Items that have no folder specified go in this special "catch-all" folder. - - - Mapper - - - Mappen er oppdatert. - - - Gå til nettstedet - The button text that allows user to launch the website to their web browser. - - - Hjelp og tilbakemelding - - - Skjul - Hide a secret value that is currently shown (password). - - - Vennligst koble deg til internett før du fortsetter. - Description message for the alert when internet connection is required to continue. - - - Internettilkobling er påkrevd - Title for the alert when internet connection is required to continue. - - - Ugyldig superpassord. Prøv igjen. - - - Ugyldig PIN-kode. Prøv igjen. - - - Åpne - The button text that allows user to launch the website to their web browser. - - - Logg på - The login button text (verb). - - - Innlogging - Title for login page. (noun) - - - Logg av - The log out button text (verb). - - - Er du sikker på at du vil logge av? - - - Superpassord - Label for a master password. - - - Mer - Text to define that there are more options things to see. - - - Mitt hvelv - The title for the vault page. - - - Navn - Label for an entity name. - - - Nei - - - Notater - Label for notes. - - - OK - Acknowledgement. - - - Passord - Label for a password. - - - Lagre - Button text for a save operation (verb). - - - Lagrer... - Message shown when interacting with the server - - - Innstillinger - The title for the settings page. - - - Vis - Reveal a hidden value (password). - - - Gjenstanden har blitt slettet. - Confirmation message after successfully deleting a login. - - - Send inn - - - Synkroniser - The title for the sync page. - - - Tusen takk - - - Verktøy - The title for the tools page. - - - URI - Label for a uri/url. - - - Bruk fingeravtrykk for å låse opp - - - Brukernavn - Label for a username. - - - {0}-feltet er påkrevd. - Validation message for when a form field is left blank and is required to be entered. - - - {0} har blitt kopiert. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verifiser fingeravtrykket - - - Verifiser superpassordet - - - Verifiser PIN-koden - - - Versjon - - - Vis - - - Besøk nettsidene våre - - - Besøk vårt nettsted for å få hjelp, nyheter, sende oss E-poster, og/eller lære mer om å bruke Bitwarden. - - - Nettsted - Label for a website. - - - Ja - - - Konto - - - Din nye konto har blitt opprettet! Du kan nå logge på. - - - Legg til gjenstand - - - App-utvidelse - - - Bruk Bitwarden sin tilgjengelighetstjeneste for å auto-utfylle dine innlogginger på apper og på nettet. - - - Auto-utfyllingstjeneste - - - Unngå forvekslingsbare tegn - - - Bitwarden App-utvidelse - - - Den enkleste måten å legge til nye innlogginger i hvelvet ditt, er fra Bitwarden sin apputvidelse. Lær mer om å bruke bitwarden-apputvidelsen ved å navigere til "Verktøy"-skjermen. - - - Bruk Bitwarden i Safari og i andre apper for å auto-utfylle dine innlogginger. - - - Bitwarden Auto-utfyllingstjeneste - - - Bruk Bitwarden sin tilgjengelighetstjeneste for å auto-utfylle dine innlogginger. - - - Endre E-postadresse - - - Du kan endre din kontos E-postadresse på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå? - - - Endre superpassordet - - - Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå? - - - Lukk - - - Kommer snart! - - - Fortsett - - - Kopiert! - - - Kopierte passordet! - - - Kopierte brukernavnet! - - - Opprett en konto - - - Oppretter konto... - Message shown when interacting with the server - - - Rediger gjenstanden - - - Aktiver automatisk synkronisering - - - Skriv inn din kontos E-postadresse for å motta hintet til ditt superpassord. - - - Aktiver apputvidelsen på nytt - - - Nesten ferdig! - - - Aktiver apputvidelsen - - - I Safari, finn Bitwarden ved å bruke deleikonet (Hint: Skroll til høyre på menyens bunnrekke). - Safari is the name of apple's web browser - - - Få umiddelbar tilgang til dine passord! - - - Du er klar til å logge på! - - - Se støttede apper - - - Dine innlogginger er nå lett tilgjengelige fra Safari, Chrome, og mange andre støttede apper. - - - I Safari og Chrome, finn Bitwarden ved å bruke deleikonet (Hint: Skroll til høyre på delemenyens bunnrekke). - - - Tast på Bitwarden-ikonet i menyen for å starte utvidelsen. - - - For å skru på Bitwarden i Safari og i andre apper, trykk på "Mer"-ikonet på bunnrekken i menyen. - - - Favoritt - - - Fingeravtrykk - - - Generer et passord - - - Få et hint om superpassordet - - - Importer gjenstander - - - Du kan importere gjenstander i bunker på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå? - - - Importer raskt dine gjenstander i bunker fra andre passordbehandlingsapper. - - - Forrige synkronisering: - - - Lengde - - - Lås - - - 15 minutter - - - 1 time - - - 1 minutt - - - 4 timer - - - Umiddelbart - - - Låsingsinnstillinger - - - Logger på... - Message shown when interacting with the server - - - Logg på eller opprett en konto for å få tilgang til ditt sikre hvelv. - - - Behandle - - - Passordbekreftelsen er ikke riktig. - - - Superpassordet er passordet du bruker for å få tilgang til hvelvet ditt. Det er veldig viktig at du aldri glemmer ditt superpassord. Det er ingen måter å få tilbake passordet på dersom du noensinne skulle klare å glemme det. - - - Et hint for superpassordet (valgfritt) - - - Et hint for superpassordet, kan hjelpe deg å huske på passordet hvis du skulle glemme det. - - - Superpassordet må være ≥8 tegn lang. - - - Minst antall numre - Minimum numeric characters for password generator settings - - - Minst antall spesialtegn - Minimum special characters for password generator settings - - - Flere innstillinger - - - Du må logge inn på Bitwarden sin hovedapp før du kan bruke utvidelsen. - - - Aldri - - - En ny gjenstand er opprettet. - - - Det er ingen favoritter i hvelvet ditt. - - - Det er ingen gjenstander i hvelvet ditt. - - - Det er ingen oppføringer i ditt hvelv for dette nettstedet. Trykk for å legge til en. - - - Denne innloggingen har verken et brukernavn eller passord satt opp. - - - OK, skjønner! - Confirmation, like "Ok, I understand it" - - - Alternativenes standardvalg er bestemt av passordgeneratorverktøyet til Bitwarden sin hovedapp. - - - Alternativer - - - Annet - - - Passord er generert. - - - Passordgenerator - - - Passordhint - - - Vi har sendt deg en E-post med hintet til superpassordet. - - - Er du sikker på at du vil overskrive det nåværende passordet? - - - Bitwarden holder ditt hvelv automatisk synkronisert ved å bruke push-beskjeder. For den beste opplevelsen, vennligst velg "Tillat" på den følgende oppspretten når du blir bedt om å aktivere push-beskjeder. - Push notifications for apple products - - - Vurder denne appen - - - Tenk gjerne på om du vil skrive en anmeldelse om oss! - - - App Store-rangeringer blir tilbakestilt etter hver nye versjon av Bitwarden. Tenk gjerne på om du vil skrive en anmeldelse om oss! - - - Omgenerer et passord - - - Skriv inn superpassordet på nytt - - - Søk i hvelvet - - - Sikkerhet - - - Se utviklingsfremgangen - - - Velg - - - Velg PIN - - - Skriv inn en 4-sifret PIN-kode til å låse opp appen med. - - - Gjenstandsinformasjon - - - Gjenstanden er oppdatert. - - - Sender inn... - Message shown when interacting with the server - - - Synkroniserer... - Message shown when interacting with the server - - - Synkronisering fullført. - - - Synkronisering mislyktes. - - - Synkroniser hvelvet nå - - - Touch ID - What Apple calls their fingerprint reader. - - - 2-trinnsinnlogging - - - 2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, E-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres på bitwarden.com-netthvelvet. Vil du besøke den nettsiden nå? - - - Lås opp med {0} - - - Lås opp med PIN-kode - - - Validerer - Message shown when interacting with the server - - - Verifiseringskode - - - Vis gjenstand - - - Bitwarden Netthvelv - - - Behandle dine gjenstander fra enhver nettleser med Bitwarden sitt netthvelv. - - - Har du mistet din autentiseringsapp? - - - Gjenstander - Screen title - - - Utvidelsen er aktivert! - - - Ikoner - - - Oversettelser - - - Gjenstander for {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Det er ingen gjenstander i hvelvet ditt for {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Når du ser en auto-utfyllingsbeskjed fra Bitwarden, kan du trykke på den for å starte auto-utfyllingstjenesten. - - - Trykk på denne beskjeden for å auto-utfylle en gjenstand fra ditt hvelv. - - - Åpne tilgjengelighetsinnstillinger - - - 1) På Android sin meny for tilgjengelighetsinnstillinger, trykk på "Bitwarden" i Tjenester-listen. - - - 2) Skru på bryteren, og trykk på OK for å akseptere. - - - Deaktivert - - - Aktivert - - - Status - - - Beta - - - Den enkleste måten å legge til nye innlogginger i hvelvet ditt, er fra Bitwarden sin auto-utfyllingstjeneste. Lær mer om å bruke Bitwarden sin auto-utfyllingstjeneste ved å navigere til "Verktøy"-skjermen. - - - Auto-utfylling - - - Vil du auto-utfylle eller vise denne gjenstanden? - - - Er du sikker på at du vil auto-utfylle denne gjenstanden? Den er ikke en full samsvaring med "{0}". - - - Samsvarende gjenstander - - - Mulig samsvarende elementer - - - Søk - - - Du søker etter en auto-utfyllingsgjenstand for "{0}". - - - Del hvelvet ditt - - - Opprett en organisasjon for å sikkert dele dine gjenstander med andre brukere. - - - Skann når passordfeltet er i fokus - - - Bare skann skjermen for felter, og tilby en auto-utfyllingsbeskjed når enn du velger et passordfelt. Denne innstillingen kan hjelpe til med å bevare batteritiden. - - - Få varslingen til å vedvare - - - Tilby alltid en auto-utfyllingsbeskjed og skann kun for felter etter å ha forsøkt på en auto-utfylling. Denne innstillingen kan hjelpe med å konservere batterilevetiden. - - - Alltid skann - - - Alltid skann skjermen etter felter, og tilby kun en auto-utfyllingsbeskjed dersom passordfelter blir funnet. Dette er standardinnstillingen. - - - Kan ikke åpne appen "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Autentiseringsapp - For 2FA - - - Skriv inn den 6-sifrede verifiseringskoden som står på din autentiseringsapp. - For 2FA - - - Skriv inn den 6-sifrede verifiseringskoden som ble sendt til {0}. - For 2FA - - - Innloggingen er utilgjengelig - For 2FA whenever there are no available providers on this device. - - - Denne kontoen har 2-trinnsinnlogging aktivert, men ingen av de oppsatte 2-trinnsleverandørene er støttet på denne enheten. Vennligst bruk en støttet enhet og/eller legg til flere leverandører som er bedre støttet på flere enheter (slik som en autentiseringsapp). - - - Gjenopprettingskode - For 2FA - - - Husk på meg - Remember my two-step login - - - Send E-posten med verifiseringskoden på nytt - For 2FA - - - 2-trinnsinnloggingsalternativer - - - Bruk en annen 2-trinnsinnloggingsmetode - - - Kunne ikke sende verifiserings-E-posten. Prøv igjen. - For 2FA - - - Verifiserings-E-posten er sendt. - For 2FA - - - For å fortsette, hold din YubiKey NEO opp mot baksiden av enheten din, eller sett inn din YubiKey i din enhets USB-inntak, og så trykk på dens knapp. - - - YubiKey-sikkerhetsnøkkel - "YubiKey" is the product name and should not be translated. - - - Legg til et nytt vedlegg - - - Vedlegg - - - Kunne ikke laste ned filen. - - - Enheten din kan ikke åpne denne filtypen. - - - Laster ned... - Message shown when downloading a file - - - Dette vedlegget er {0} stort. Er du sikker på at du vil laste det ned til enheten din? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Autentiseringsnøkkel (TOTP) - - - Verifiseringskode (TOTP) - Totp code label - - - Autentiseringsnøkkel er lagt til. - - - Kan ikke lese autentiseringsnøkkelen. - - - Skanning vil forekomme automatisk. - - - Pek kameraet ditt mot QR-koden. - - - Skann QR-koden - - - Kamera - - - Bilder - - - Kopierte TOTP-en! - - - Kopier TOTP-en - - - Dersom din innlogging har en autentiseringsnøkkel knyttet til den, blir TOTP-verifiseringskoden automatisk kopiert til utklippstavlen din når enn du auto-utfyller innloggingen. - - - Deaktiver automatisk TOTP-kopiering - - - Et Premium-medlemskap er påkrevd for å bruke denne funksjonen. - - - Vedlegget er lagt til - - - Vedlegget er slettet - - - Velg fil - - - Fil - - - Ingen fil er valgt - - - Det er ingen vedlegg. - - - Filens kilde - - - Egenskapen er utilgjengelig - - - Maksimal filstørrelse er 100 MB. - - - Du kan ikke bruke denne funksjonen før du oppdaterer krypteringsnøkkelen din. - - - Lær mer - - - API-tjenernettadresse - - - Tilpasset miljø - - - For avanserte brukere. Du kan bestemme grunn-nettadressen til hver tjeneste separat. - - - Miljø-nettadressene har blitt lagret. - - - {0} er ikke riktig formattert. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identitetstjenerens nettadresse - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Selvbetjent miljø - - - Spesifiser grunn-nettadressen til din selvbetjente Bitwarden-installasjon. - - - Tjener-nettadresse - - - Netthvelvets tjenernettadresse - - - Trykk på denne beskjeden for å se gjenstander fra ditt hvelv. - - - Tilpassede felter - - - Kopier nummeret - - - Kopier sikkerhetskoden - - - Nummer - - - Sikkerhetskode - - - Hva slags gjenstandstype vil du legge til? - - - Kort - - - Identitet - - - Innlogging - - - Sikker notis - - - Adresse 1 - - - Adresse 2 - - - Adresse 3 - - - April - - - August - - - Merke - - - Kortholderens navn - - - By / Tettsted - - - Firma - - - Land - - - Desember - - - Dr. - - - Utløpsmåned - - - Utløpsår - - - Februar - - - Fornavn - - - Januar - - - Juli - - - Juni - - - Etternavn - - - Lisensnummer - - - Mars - - - Mai - - - Mellomnavn - - - Herr - - - Fru - - - Frøken - - - November - - - Oktober - - - Pass-nummer - - - Telefon - - - September - - - Nummer for sosial stønad - - - Fylke / Region - - - Tittel - - - Postnummer - - - Adresse - - - Utløp - - - Skru av nettstedsikoner - - - Nettstedsikoner sørger for et gjenkjennelig bilde ved siden av hver innloggingsgjenstand i hvelvet ditt. - - - Ikonenes tjenernettadresse - - - Auto-utfyll med Bitwarden - - - Hvelvet er låst - - - Gå til hvelvet mitt - - - Samlinger - - - Det er ingen gjenstander i denne samlingen. - - - Det er ingen gjenstander i denne mappen. - - - Auto-utfyllingstilgjengelighetstjeneste - - - Bitwarden sin auto-utfyllingstjeneste bruker Android Autofill Framework for å bidra til å fylle inn innlogginger, bankkort og identifikasjonsinfo inn i andre apper på din enhet. - - - Bruk Bitwarden sin auto-utfyllingstjeneste for å fylle ut innlogginger, bankkort og identifikasjonsinfo i andre apper. - - - Åpne auto-utfyllingsinnstillingene - - - Face ID - What Apple calls their facial recognition reader. - - - Bruk Face ID for å verifisere. - - - Bruk Face ID til å låse opp - - - Verifiser din Face ID - - - Lås opp med Windows Hello - - - Verifiser med Windows Hello - - - Windows Hello - - - Vi klarte ikke å automatisk åpne Android sine auto-utfyllingsinnstilinger for deg. Du kan bla manuelt til auto-utfyllingsinnstillingsmenyen fra Android-innstillinger → System → Språk og inndata → Avansert → Auto-utfyllingstjeneste. - - - Navn på spesifikt felt - - - Boolsk verdi - - - Skjult - - - Tekst - - - Nytt spesifikt felt - - - Hvilken type spesifikt felt vil du legge til? - - - Fjern - - - Ny URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Grunndomene - - - Standard - - - Nøyaktig - - - Vert - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regulært uttrykk - A programming term, also known as 'RegEx'. - - - Starter med - - - URI-matchgjenkjenning - - - Match-gjenkjenning - URI match detection for auto-fill. - - - Ja, og lagre - - - Autofyll og lagre - - - Organisasjon - An entity of multiple related people (ex. a team or business organization). - - - Hold din Yubikey nær toppen av enheten. - - - Prøv igjen - - - For å fortsette, hold din YubiKey NEO mot baksiden av enheten. - - - Tilgjengelighetstjenesten kan være nyttig å bruke når apper ikke støtter den vanlige autoutfyllingstjenesten. - - - Passordet ble oppdatert den - ex. Date this password was updated - - - Oppdatert den - ex. Date this item was updated - - - Autoutfylling aktivert! - - - Du må logge deg inn på Bitwarden-hovedappen før du kan bruke autoutfylling. - - - Innloggingene dine er nå lett tilgjengelige fra tastaturet når du skal logge deg inn på apper og nettsteder. - - - Vi anbefaler å deaktivere alle andre autoutfyllingsapper i Innstillinger dersom du ikke har tenkt å bruke dem. - - - Få tilgang til hvelvet ditt direkte fra tastaturet for å raskt autoutfylle passord. - - - Følg disse instruksjonene for å aktivere passord-autoutfylling på enheten din: - - - 1. Gå til iOS-appen «Innstillinger» - - - 2. Trykk på «Passord og kontoer» - - - 3. Trykk «Autoutfyll passord» - - - 4. Slå på Autoutfyll - - - 5. Velg «Bitwarden» - - - Passord-autoutfylling - - - Den enkleste måten å legge til nye innlogginger i hvelvet ditt, er å bruke «Bitwarden-passordautoutfylling»-utvidelsen. Lær mer om «Bitwarden-passordautoutfylling»-utvidelsen ved å gå til «Verktøy»-siden. - - diff --git a/src/App/Resources/AppResources.nl.Designer.cs b/src/App/Resources/AppResources.nl.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.nl.resx b/src/App/Resources/AppResources.nl.resx deleted file mode 100644 index eed0fc7dd..000000000 --- a/src/App/Resources/AppResources.nl.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Over - - - Toevoegen - Add/create a new entity (verb). - - - Map toevoegen - - - Item toevoegen - The title for the add item page. - - - Er is een fout opgetreden. - Alert title when something goes wrong. - - - Terug - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Annuleren - Cancel an operation. - - - Kopiëren - Copy some value to your clipboard. - - - Wachtwoord kopiëren - The button text that allows a user to copy the login's password to their clipboard. - - - Gebruikersnaam kopiëren - The button text that allows a user to copy the login's username to their clipboard. - - - Met dank aan - Title for page that we use to give credit to resources that we use. - - - Verwijderen - Delete an entity (verb). - - - Bezig met verwijderen... - Message shown when interacting with the server - - - Weet u zeker dat u dit wilt verwijderen? Dit kan niet ongedaan worden gemaakt. - Confirmation alert message when deleteing something. - - - Bewerken - - - Map bewerken - - - E-mail - Short label for an email address. - - - E-mailadres - Full label for a email address. - - - E-mail ons - - - Verstuur ons een e-mail om hulp te krijgen of feedback te geven. - - - Voer uw PIN-code in. - - - Favorieten - Title for your favorite items in the vault. - - - Meld een fout (bug) - - - Open een issue op onze GitHub-pagina. - - - Gebruik uw vingerafdruk om te verifiëren. - - - Map - Label for a folder. - - - Nieuwe map is gecreëerd. - - - Map is verwijderd. - - - Geen map - Items that have no folder specified go in this special "catch-all" folder. - - - Mappen - - - Map is bijgewerkt. - - - Ga naar website - The button text that allows user to launch the website to their web browser. - - - Hulp en feedback - - - Verbergen - Hide a secret value that is currently shown (password). - - - Verbindt met het internet alvorens door te gaan. - Description message for the alert when internet connection is required to continue. - - - Internetverbinding vereist - Title for the alert when internet connection is required to continue. - - - Ongeldig hoofdwachtwoord. Probeer het opnieuw. - - - Ongeldige PIN-code. Probeer het opnieuw. - - - Starten - The button text that allows user to launch the website to their web browser. - - - Inloggen - The login button text (verb). - - - Inloggen - Title for login page. (noun) - - - Uitloggen - The log out button text (verb). - - - Weet u zeker dat u wilt uitloggen? - - - Hoofdwachtwoord - Label for a master password. - - - Meer - Text to define that there are more options things to see. - - - Mijn kluis - The title for the vault page. - - - Naam - Label for an entity name. - - - Nee - - - Notities - Label for notes. - - - Oké - Acknowledgement. - - - Wachtwoord - Label for a password. - - - Opslaan - Button text for a save operation (verb). - - - Bezig met opslaan... - Message shown when interacting with the server - - - Instellingen - The title for the settings page. - - - Weergeven - Reveal a hidden value (password). - - - Item is verwijderd. - Confirmation message after successfully deleting a login. - - - Versturen - - - Synchroniseren - The title for the sync page. - - - Bedankt - - - Hulpmiddelen - The title for the tools page. - - - URI - Label for a uri/url. - - - Vingerafdruk gebruiken om te ontgrendelen - - - Gebruikersnaam - Label for a username. - - - Het {0}-veld is vereist. - Validation message for when a form field is left blank and is required to be entered. - - - {0} is gekopieerd. - Confirmation message after suceessfully copying a value to the clipboard. - - - Vingerafdruk verifiëren - - - Hoofdwachtwoord verifiëren - - - PIN-code verifiëren - - - Versie - - - Weergeven - - - Bezoek onze website - - - Bezoek onze website voor hulp, nieuws, om ons een mail te sturen en/of meer te weten te komen over het gebruik van Bitwarden. - - - Website - Label for a website. - - - Ja - - - Account - - - Uw account is gecreëerd! U kunt nu inloggen. - - - Voeg een item toe - - - App-extensie - - - Gebruik de Bitwarden-toegankelijkheidsdienst om uw inloggegevens automatisch in te laten vullen in de apps en op het internet. - - - Auto-invullen-dienst - - - Onduidelijke tekens vermijden - - - Bitwarden app-extensie - - - De eenvoudigste manier om nieuwe logins toe te voegen aan uw kluis is door de Bitwarden app-extensie te gebruiken. Lees meer over het gebruik van de app-extensie op het "Hulpmiddelen"-scherm. - - - Gebruik Bitwarden in Safari en andere apps om uw inloggegevens automatisch in te vullen. - - - Bitwarden auto-invullen-dienst - - - Gebruik de Bitwarden-toegankelijkheidsdienst om uw inloggegevens automatisch in te laten vullen. - - - E-mailadres wijzigen - - - U kunt uw e-mailadres wijzigen in de kluis op bitwarden.com Wilt u de website nu bezoeken? - - - Hoofdwachtwoord wijzigen - - - U kunt uw hoofdwachtwoord wijzigen in de kluis op bitwarden.com Wilt u de website nu bezoeken? - - - Sluiten - - - Binnenkort beschikbaar! - - - Doorgaan - - - Gekopieerd! - - - Wachtwoord is gekopieerd! - - - Gebruikersnaam is gekopieerd! - - - Account creëren - - - Bezig met creëren van account... - Message shown when interacting with the server - - - Item bewerken - - - Automatische synchronisatie inschakelen - - - Voer het e-mailadres van uw account in om uw hoofdwachwoord-hint te ontvangen. - - - App-extensie opnieuw inschakelen - - - Bijna klaar! - - - App-extensie inschakelen - - - In Safari vindt u Bitwarden onder het deel-pictogram (hint: scroll naar rechts op de onderste rij van het menu). - Safari is the name of apple's web browser - - - Verkrijg onmiddellijke toegang tot uw wachtwoorden! - - - U bent klaar om in te loggen! - - - Ondersteunde apps bekijken - - - Uw inloggevens zijn nu gemakkelijk bereikbaar vanuit Safari, Chrome en ondersteunde apps. - - - In Safari en Chrome vindt u Bitwarden onder het deel-pictogram (hint: scroll naar rechts op de onderste rij van het menu). - - - Raak het Bitwarden-pictogram in het menu aan om de extensie te starten. - - - Raak het "meer"-pictogram op de onderste rij van het menu aan om Bitwarden in te schakelen in Safari en andere apps. - - - Favoriet - - - Vingerafdruk - - - Wachtwoord genereren - - - Verkrijg hoofdwachtwoord-hint - - - Items importeren - - - U kunt een bulk import van items doen vanuit de bitwarden.com webkluis. Wilt u de website nu bezoeken? - - - Importeer snel meerdere items uit andere wachtwoordbeheer-apps. - - - Laatste synchronisatie: - - - Lengte - - - Vergrendelen - - - 15 minuten - - - 1 uur - - - 1 minuut - - - 4 uur - - - Onmiddelijk - - - Vergrendelopties - - - Bezig met inloggen... - Message shown when interacting with the server - - - Log in of creëer een nieuw account om toegang te krijgen tot uw beveiligde kluis. - - - Beheren - - - De wachtwoorden komen niet overeen. - - - Het hoofdwachtwoord is het wachtwoord waarmee u toegang krijgt tot uw kluis. Het is belangrijk dat u het hoofdwachtwoord niet vergeet want er is geen manier om het te herstellen. - - - Hoofdwachtwoord-hint (optioneel) - - - Een hoofdwachtwoord-hint kan u helpen uw wachtwoord te herinneren als u hem vergeten bent. - - - Het hoofdwachtwoord moet minimaal 8 tekens bevatten. - - - Minimum aantal getallen - Minimum numeric characters for password generator settings - - - Minimum aantal speciale tekens - Minimum special characters for password generator settings - - - Meer instellingen - - - U moet inloggen op de Bitwarden-app voordat u de extensie kunt gebruiken. - - - Nooit - - - Nieuw item gecreëerd. - - - Er zijn geen favorieten in uw kluis. - - - Er zijn geen items in uw kluis. - - - Er zijn geen items in uw kluis voor deze website. Klik om gegevens toe te voegen. - - - Deze login bevat geen gebruikersnaam of wachtwoord. - - - Oké, ik begrijp het! - Confirmation, like "Ok, I understand it" - - - De standaardwaarden van de optie kunnen worden ingesteld met het wachtwoordgenerator-hulpmiddel in de app. - - - Opties - - - Overig - - - Wachtwoord is gegenereerd. - - - Wachtwoordgenerator - - - Wachtwoordhint - - - We hebben u een e-mail gestuurd met uw hoofdwachtwoord-hint. - - - Weet u zeker dat het huidige wachtwoord wilt overschrijven? - - - Bitwarden houdt uw kluis automatisch gesynchroniseerd d.m.v. push-meldingen. Druk in het volgende dialoogvenster op "Oké" om push-meldingen in te schakelen. - Push notifications for apple products - - - De app waarderen - - - Help ons door een goede recensie te schrijven! - - - App Store-beoordelingen worden gereset bij elke nieuwe versie van Bitwarden. Help ons door een goede beoordeling te schrijven! - - - Wachtwoord opnieuw genereren - - - Hoofdwachtwoord opnieuw invoeren - - - Kluis doorzoeken - - - Beveiliging - - - Bekijk de ontwikkelingsvoortgang - - - Selecteren - - - PIN-code instellen - - - Voer een PIN-code in van 4 tekens om de app mee te ontgrendelen. - - - Item-informatie - - - Item geüpdatet. - - - Bezig met versturen... - Message shown when interacting with the server - - - Bezig met synchroniseren... - Message shown when interacting with the server - - - Synchroniseren voltooid. - - - Synchroniseren mislukt. - - - Kluis nu synchroniseren - - - Touch ID - What Apple calls their fingerprint reader. - - - Inloggen in twee stappen - - - Inloggen in twee stappen beveiligt uw account beter doordat u uw inlogpoging moet bevestigen met een ander apparaat. Dit kan bijv. een beveiligingssleutel zijn, maar een verificatie-app, SMS, spraakoproep of e-mail. Inloggen in twee stappen kan worden ingeschakeld in de kluis op bitwarden.com Wilt u de website nu bezoeken? - - - Ontgrendelen met {0} - - - Ontgrendelen met PIN-code - - - Bezig met valideren - Message shown when interacting with the server - - - Verificatiecode - - - Item weergeven - - - Bitwarden-webkluis - - - Beheer uw items vanuit elke webbrowser met de Bitwarden-webkluis. - - - Authenticatie-app kwijt? - - - Items - Screen title - - - Extensie is geactiveerd! - - - Pictogrammen - - - Vertalingen - - - Items voor {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Er zijn geen items in uw kluis voor {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Als u een Bitwarden auto-invullen-melding ziet, dan kunt u deze aanraken om de auto-invullen-dienst te starten. - - - Raak deze melding aan om een login automatisch in te vullen d.m.v. uw kluis. - - - Toegankelijkheidsinstellingen openen - - - 1. Raak op het Android-toegankelijkheidsinstellingen-scherm "Bitwarden" aan onder de kop Diensten. - - - 2. Raak de schuifknop aan en druk op Oké om te accepteren. - - - Uitgeschakeld - - - Ingeschakeld - - - Status - - - Bèta - - - De eenvoudigste manier om nieuwe logins toe te voegen aan uw kluis is door de Bitwarden app-extensie te gebruiken. Lees meer over het gebruik van de app-extensie op het "Hulpmiddelen"-scherm. - - - Auto-invullen - - - Wilt u deze login auto-invullen of weergeven? - - - Weet u zeker dat u deze login automatisch wilt invullen? Het is geen volledige overeenkomst voor "{0}". - - - Overeenkomende items - - - Mogelijke overeenkomende items - - - Zoeken - - - U bent aan het zoeken voor een auto-invul-login voor "{0}". - - - Deel uw kluis - - - Creëer een organisatie om uw logins veilig te delen met andere gebruikers. - - - Scannen als wachtwoordveld gefocust is - - - Alleen zoeken naar velden en een auto-invul-melding weergeven zodra u een wachtwoordveld selecteert. Deze instelling kan accubesparend werken. - - - Aanhoudende melding - - - Altijd een auto-invul-melding weergeven en alleen zoeken naar velden na het proberen van automatisch invullen. Deze instelling kan accubesparend werken. - - - Altijd scannen - - - Altijd zoeken naar velden en alleen een auto-invul-melding weergeven als er wachtwoordvelden gevonden zijn. Dit is de standaard instelling. - - - De app "{0}" kan niet worden geopend. - Message shown when trying to launch an app that does not exist on the user's device. - - - Authenticatie-app - For 2FA - - - Voer de 6-cijferige verificatiecode in die in uw authenticatie-app staat. - For 2FA - - - Voer de 6-cijferige verificatiecode in die gemaild is naar {0}. - For 2FA - - - Login niet beschikbaar - For 2FA whenever there are no available providers on this device. - - - Dit account heeft inloggen in twee stappen ingeschakeld, maar geen van de ingestelde diensten worden ondersteund op dit apparaat. Gebruik een ondersteund apparaat en/of extra diensten die beter worden ondersteund op apparaten, zoals een authenticatie-app. - - - Herstelcode - For 2FA - - - Mij onthouden - Remember my two-step login - - - Verificatiecode-e-mail opnieuw versturen - For 2FA - - - Inloggen in twee stappen-opties - - - Andere methode gebruiken voor inloggen in twee stappen - - - De verificatie-e-mail kan niet worden verstuurd. Probeer het opnieuw. - For 2FA - - - Verificatie-e-mail is verstuurd. - For 2FA - - - Houdt uw YubiKey NEO tegen de achterkant van het apparaat om door te gaan. - - - YubiKey NEO-beveiligingssleute - "YubiKey" is the product name and should not be translated. - - - Nieuwe bijlage toevoegen - - - Bijlagen - - - Bestand kan niet worden gedownload. - - - Uw apparaat kan dit type bestand niet openen. - - - Bezig met downloaden... - Message shown when downloading a file - - - Deze bijlage heeft een grootte van {0}. Weet u zeker dat u deze wilt downloaden naar uw apparaat? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Authenticatie-sleutel (TOTP) - - - Verificatiecode (TOTP) - Totp code label - - - Authenticatie-sleutel is toegevoegd. - - - De authenticatie-sleutel kan niet worden gelezen. - - - Het scannen gaat automatisch. - - - Richt uw camera op de QR-code. - - - QR-code scannen - - - Camera - - - Foto's - - - TOTP is gekopieerd! - - - TOTP kopiëren - - - Als uw login beschikt over een sleutel van de verificator, dan wordt de TOTP-verificatiecode automatisch gekopieerd naar uw klembord wanneer de login automatisch wordt ingevuld. - - - TOTP automatisch kopiëren uitschakelen - - - U heeft een Premium-lidmaatschap nodig om deze functie te kunnen gebruiken. - - - Bijlage is toegevoegd - - - Bijlage is verwijderd - - - Bestand kiezen - - - Bestand - - - Geen bestand gekozen - - - Er zijn geen bijlagen. - - - Bestandsbron - - - Functie niet beschikbaar - - - De maximale bestandsgrootte is 100 MB. - - - U kunt deze functie niet gebruiken zolang u uw encryptiesleutel niet hebt bijgewerkt. - - - Meer informatie - - - API server-URL - - - Aangepaste omgeving - - - Voor gevorderde gebruikers. Je kan de basis-URL van iedere service afzonderlijk instellen. - - - De omgeving URL's zijn opgeslagen. - - - {0} is niet correct opgemaakt. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Gebruikersbeheer server-URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Zelfgehoste omgeving - - - Specificeer de basis-URL van uw op locatie gehoste Bitwarden-installatie. - - - Server-URL - - - Webkluis server-URL - - - Raak deze melding aan om logins te bekijken uit uw kluis. - - - Aangepaste velden - - - Nummer Kopiëren - - - Beveiligingscode kopiëren - - - Nummer - - - Beveiligingscode - - - Wat voor type item wilt u toevoegen? - - - Kaart - - - Identiteit - - - Login - - - Veilige notitie - - - Adres 1 - - - Adres 2 - - - Adres 3 - - - April - - - Augustus - - - Merk - - - Naam van kaarthouder - - - Stad / Dorp - - - Bedrijf - - - Land - - - December - - - Dr. - - - Vervalmaand - - - Vervaljaar - - - Februari - - - Voornaam - - - Januari - - - Juli - - - Juni - - - Achternaam - - - Rijbewijsnummer - - - Maart - - - Mei - - - Tweede voornaam - - - Dhr. - - - mevr. - - - Mej. - - - November - - - Oktober - - - Paspoortnummer - - - Telefoonnummer - - - September - - - Burgerservicenummer - - - Staat / Provincie - - - Titel - - - Postcode - - - Adres - - - Vervaldatum - - - Websitepictogrammen uitschakelen - - - Websitepictogrammen bieden een herkenbare afbeelding naast elk item in uw kluis. - - - Pictogrammen server-URL - - - Automatisch invullen met Bitwarden - - - Kluis is vergrendeld - - - Ga naar mijn kluis - - - Verzamelingen - - - Er zijn geen items in deze verzameling. - - - Er zijn geen items in deze map. - - - Automatisch invullen door Toegankelijkheidsservice - - - De Bitwarden automatische invulservice gebruikt Android Autofill Framework om u te helpen bij het invullen van inloggegevens, creditcards en identiteitsgegevens in andere apps op uw apparaat. - - - Gebruik de Bitwarden-toegankelijkheidsdienst om uw inloggevens automatisch in te laten vullen. - - - Open instellingen voor automatisch invullen - - - Face ID - What Apple calls their facial recognition reader. - - - Gebruik Face ID om te verifiëren. - - - Gebruik Face ID om te ontgrendelen - - - Verifieer Face ID - - - Ontgrendelen met Windows Hello - - - Verifiëren met Windows Hello - - - Windows Hello - - - Het automatisch openen van het Android automatisch invullen-instellingenmenu is mislukt. U kunt het menu handmatig openen via Android-instellingen > Systeem > Taal en invoer > Geavanceerd > Automatisch aanvullen-dienst. - - - Naam van aangepast veld - - - Boolean - - - Verborgen - - - Tekst - - - Nieuw aangepast veld - - - Wat voor soort aangepast veld wilt u toevoegen? - - - Verwijderen - - - Nieuwe URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Basisdomein - - - Standaard - - - Exact - - - Hostnaam - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Reguliere expressie - A programming term, also known as 'RegEx'. - - - Begint met - - - URI-overeenkomstdetectie - - - Overeenkomstdetectie - URI match detection for auto-fill. - - - Ja, en opslaan - - - Automatisch invullen en opslaan - - - Organisatie - An entity of multiple related people (ex. a team or business organization). - - - Houd uw Yubikey bij de bovenkant van het apparaat. - - - Probeer het opnieuw - - - Houd uw YubiKey NEO bij de achterkant van het apparaat om door te gaan. - - - De toegankelijkheidsdienst kan nuttig zijn wanneer apps het reguliere automatisch invullen niet ondersteunen. - - - Wachtwoord bijgewerkt - ex. Date this password was updated - - - Bijgewerkt - ex. Date this item was updated - - - Automatisch invullen geactiveerd! - - - U moet inloggen op de Bitwarden-app voordat u Automatisch invullen kunt gebruiken. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - Het is raadzaam Automatisch invullen door andere apps uit te schakelen als u niet van plan bent om deze te gebruiken. - - - Open uw kluis rechtstreeks vanaf uw toetsenbord om wachtwoorden snel automatisch te laten invullen. - - - Voer de volgende stappen uit om Automatisch invullen van wachtwoorden in te schakelen: - - - 1. Ga naar de iOS-app "Instellingen" - - - 2. Tik op "Wachtwoorden & Accounts" - - - 3. Tik op "Wachtwoorden automatisch invullen" - - - 4. Activeer Automatisch invullen - - - 5. Selecteer "Bitwarden" - - - Wachtwoord automatisch invullen - - - De eenvoudigste manier om nieuwe aanmeldingen aan uw kluis toe te voegen, is met behulp van de Bitwarden Password AutoFill-extensie. Meer informatie over het gebruik van de AutoFill-extensie Bitwarden-wachtwoord door naar het scherm "Gereedschap" te gaan. - - diff --git a/src/App/Resources/AppResources.pl.Designer.cs b/src/App/Resources/AppResources.pl.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.pl.resx b/src/App/Resources/AppResources.pl.resx deleted file mode 100644 index 6a6151370..000000000 --- a/src/App/Resources/AppResources.pl.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - O programie - - - Dodaj - Add/create a new entity (verb). - - - Dodaj folder - - - Dodaj element - The title for the add item page. - - - Wystąpił błąd. - Alert title when something goes wrong. - - - Powrót - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Anuluj - Cancel an operation. - - - Kopiuj - Copy some value to your clipboard. - - - Skopiuj hasło - The button text that allows a user to copy the login's password to their clipboard. - - - Skopiuj nazwę użytkownika - The button text that allows a user to copy the login's username to their clipboard. - - - Podziękowania - Title for page that we use to give credit to resources that we use. - - - Usuń - Delete an entity (verb). - - - Usuwanie... - Message shown when interacting with the server - - - Czy na pewno chcesz to usunąć? Tej operacji nie można cofnąć. - Confirmation alert message when deleteing something. - - - Edycja - - - Edytuj Folder - - - Email - Short label for an email address. - - - Adres email - Full label for a email address. - - - Napisz do nas - - - Napisz do nas, jeśli potrzebujesz pomocy lub chcesz przesłać opinię. - - - Wprowadź kod PIN. - - - Ulubione - Title for your favorite items in the vault. - - - Zgłoś błąd - - - Zgłoś problem na naszym Githubie. - - - Użyj odcisku palca do weryfikacji. - - - Folder - Label for a folder. - - - Utworzono nowy folder. - - - Folder usunięty. - - - Nieprzypisane - Items that have no folder specified go in this special "catch-all" folder. - - - Foldery - - - Folder zaktualizowany. - - - Przejdź do strony - The button text that allows user to launch the website to their web browser. - - - Pomoc i Opinie - - - Ukryj - Hide a secret value that is currently shown (password). - - - Połącz się z internetem zanim przejdziesz dalej. - Description message for the alert when internet connection is required to continue. - - - Wymagane połączenie z Internetem - Title for the alert when internet connection is required to continue. - - - Nieprawidłowe hasło główne. Spróbuj ponownie. - - - Błędny kod PIN. Spróbuj ponownie. - - - Uruchom - The button text that allows user to launch the website to their web browser. - - - Zaloguj się - The login button text (verb). - - - Logowanie - Title for login page. (noun) - - - Wyloguj się - The log out button text (verb). - - - Czy na pewno chcesz się wylogować? - - - Hasło Główne - Label for a master password. - - - Więcej - Text to define that there are more options things to see. - - - Sejf - The title for the vault page. - - - Nazwa - Label for an entity name. - - - Nie - - - Notatki - Label for notes. - - - Ok - Acknowledgement. - - - Hasło - Label for a password. - - - Zapisz - Button text for a save operation (verb). - - - Zapisywanie... - Message shown when interacting with the server - - - Ustawienia - The title for the settings page. - - - Pokaż - Reveal a hidden value (password). - - - Element został usunięty. - Confirmation message after successfully deleting a login. - - - Prześlij - - - Synchronizacja - The title for the sync page. - - - Dziękujemy - - - Narzędzia - The title for the tools page. - - - URI - Label for a uri/url. - - - Odblokuj za pomocą odcisku palca - - - Nazwa użytkownika - Label for a username. - - - Pole {0} jest wymagane. - Validation message for when a form field is left blank and is required to be entered. - - - Skopiowano {0}. - Confirmation message after suceessfully copying a value to the clipboard. - - - Zweryfikuj odcisk palca - - - Sprawdź hasło główne - - - Sprawdź kod PIN - - - Wersja - - - Widok - - - Odwiedź naszą stronę - - - Odwiedź naszą stronę, aby uzyskać pomoc, zobaczyć aktualności, napisać do nas i/lub dowiedzieć się, jak używać Bitwarden. - - - Strona - Label for a website. - - - Tak - - - Konto - - - Twoje konto zostało utworzone! Teraz możesz się zalogować. - - - Dodaj element - - - Rozszerzenie aplikacji - - - Użyj usługi ułatwień dostępu Bitwarden, aby korzystać z automatycznego wypełniania danych logowania w aplikacjach i na stronach internetowych. - - - Usługa automatycznego wypełniania - - - Unikaj niejednoznacznych znaków - - - Rozszerzenie aplikacji Bitwarden - - - Najprostszym sposobem dodania nowych danych logowania do sejfu jest Rozszerzenie Aplikacji Bitwarden. Więcej informacji o używaniu Rozszerzenia Aplikacji Bitwarden znajdziesz w widoku "Narzędzia". - - - Użyj Bitwarden w Safari i innych aplikacjach do automatycznego wypełniania danych logowania. - - - Usługa automatycznego wypełniania Bitwarden - - - Użyj usługi ułatwień dostępu Bitwarden do automatycznego wypełniania danych logowania. - - - Zmień adres e-mail - - - Swój adres e-mail możesz zmienić w sejfie internetowym bitwarden.com. Czy chcesz teraz przejść do tej strony? - - - Zmień hasło główne - - - Swoje hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz teraz przejść do tej strony? - - - Zamknij - - - Dostępne wkrótce! - - - Kontynuuj - - - Skopiowane! - - - Hasło skopiowane! - - - Nazwa użytkownika skopiowana! - - - Utwórz Konto - - - Tworzenie konta... - Message shown when interacting with the server - - - Edytuj element - - - Włącz automatyczną synchronizację - - - Podaj e-mail powiązany z kontem, aby otrzymać podpowiedź do hasła głównego. - - - Ponownie włącz Rozszerzenie Aplikacji - - - Prawie gotowe! - - - Włącz Rozszerzenie Aplikacji - - - W Safari, znajdź Bitwarden przy użyciu ikony udostępniania (podpowiedź: przewiń dolny rząd menu do prawej strony). - Safari is the name of apple's web browser - - - Uzyskaj natychmiastowy dostęp do haseł! - - - Jesteś gotowy do zalogowania się! - - - Zobacz obsługiwane aplikacje - - - Twoje dane logowania są teraz łatwo dostępne z Safari, Chrome i innych obsługiwanych aplikacji. - - - W Safari i Chrome, znajdź Bitwarden przy użyciu ikony udostępniania (podpowiedź: przewiń dolny rząd menu udostępniania do prawej strony). - - - Naciśnij ikonę Bitwarden w menu, aby uruchomić rozszerzenie. - - - Aby włączyć Bbitwarden w Safari i innych aplikacjach, dotknij ikonę "więcej" w dolnym rzędzie menu. - - - Ulubione - - - Odcisk palca - - - Wygeneruj hasło - - - Uzyskaj podpowiedź hasła głównego - - - Importuj elementy - - - Możesz zaimportować wiele elementów z sejfu internetowego bitwarden. Czy chcesz przejść teraz do tej strony? - - - Szybko zaimportuj wiele elementów z innych menedżerów haseł. - - - Ostatnia synchronizacja: - - - Długość - - - Blokada - - - po 15 minutach - - - po 1 godzinie - - - po 1 minucie - - - po 4 godzinach - - - Natychmiast - - - Czas automatycznego blokowania - - - Logowanie... - Message shown when interacting with the server - - - Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu. - - - Zarządzaj - - - Potwierdzenie hasła nie pasuje. - - - Hasło główne to hasło, którego używasz, aby uzyskać dostęp do Twojego sejfu. Bardzo ważne jest, abyś pamiętał swoje hasło główne. Jeśli je zapomnisz, nie będzie możliwości, aby je odzyskać. - - - Podpowiedź do hasła głównego (opcjonalnie) - - - Wskazówka dotycząca hasła głównego może pomóc Ci przypomnieć sobie hasło, jeśli je zapomnisz. - - - Hasło główne musi zawierać co najmniej 8 znaków. - - - Min. liczba znaków - Minimum numeric characters for password generator settings - - - Min. liczba znaków specjalnych - Minimum special characters for password generator settings - - - Więcej ustawień - - - Musisz zalogować się do aplikacji głównej Bitwarden, zanim będziesz mógł używać rozszerzenia. - - - Nigdy - - - Utworzono nowy element. - - - Brak ulubionych w Twoim sejfie. - - - Brak elementów w Twoim sejfie. - - - Brak elementów dla tej strony w Twoim sejfie. Dotknij, aby je dodać. - - - Te dane logowania nie posiadają skonfigurowanej nazwy użytkownika lub hasła. - - - Ok, rozumiem! - Confirmation, like "Ok, I understand it" - - - Opcje domyślne są pobierane z narzędzia generatora haseł głównej aplikacji Bitwarden. - - - Opcje - - - Inne - - - Hasło wygenerowane. - - - Generator haseł - - - Podpowiedź do hasła - - - Wysłaliśmy Ci wiadomość e-mail z Twoją podpowiedzią do hasła głównego. - - - Czy na pewno chcesz zastąpić bieżące hasło? - - - Bitwarden przeprowadza automatyczną synchronizację sejfu przy użyciu powiadomień push. Aby uzyskać najlepsze możliwe wyniki, wybierz "Zezwalaj", gdy za chwilę zostaniesz poproszony o włączenie powiadomień push. - Push notifications for apple products - - - Oceń aplikację - - - Wesprzyj nas pozytywną opinią! - - - Oceny w App Store są zerowane przy każdej nowej wersji Bitwarden. Jeśli chcesz nam pomóc, napisz pozytywną recenzję! - - - Wygeneruj hasło - - - Powtórz hasło główne - - - Szukaj w sejfie - - - Zabezpieczenia - - - Zobacz postępu rozwoju - - - Wybierz - - - Ustaw kod PIN - - - Wprowadź 4-cyfrowy kod PIN do odblokowywania aplikacji. - - - Informacje o elemencie - - - Element zaktualizowany. - - - Wysyłanie... - Message shown when interacting with the server - - - Synchronizuję... - Message shown when interacting with the server - - - Synchronizacja zakończona. - - - Synchronizacja nie powiodła się. - - - Rozpocznij synchronizację sejfu - - - Touch ID - What Apple calls their fingerprint reader. - - - Dwustopniowe logowanie - - - Dwustopniowe logowanie sprawia, że Twoje konto jest bardziej bezpieczne poprzez wymuszenie potwierdzenia logowania z innego urządzenia, takiego jak klucz bezpieczeństwa, aplikacja uwierzytelniająca, SMS, telefon lub e-mail. Dwustopniowe logowanie można włączyć w internetowym sejfie bitwarden.com. Czy chcesz teraz przejść do tej strony? - - - Odblokuj poprzez {0} - - - Odblokuj kodem PIN - - - Weryfikuję - Message shown when interacting with the server - - - Kod weryfikacyjny - - - Zobacz element - - - Internetowy sejf Bitwarden - - - Zarządzaj swoimi danymi z poziomu dowolnej przeglądarki za pomocą sejfu internetowego Bitwarden. - - - Utraciłeś dostęp do aplikacji uwierzytelniającej? - - - Elementy - Screen title - - - Rozszerzenie aktywowane! - - - Ikony - - - Tłumaczenia - - - Elementy dla {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - W Twoim sejfie brak elementów dla {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Kiedy widzisz powiadomienie automatycznego wypełniania Bitwarden, możesz je dotknąć, aby uruchomić tę usługę. - - - Dotknij to powiadomienie, aby automatycznie wypełnić danymi z sejfu. - - - Otwórz ustawienia ułatwień dostępu - - - 1. Na ekranie ustawień ułatwień dostępu Android dotknij "Bitwarden" na liście usług. - - - 2. Przestaw przełącznik i naciśnij OK, aby zaakceptować. - - - Wyłączone - - - Włączone - - - Status - - - Beta - - - Najprostszym sposobem dodania nowych danych logowania do sejfu jest Rozszerzenie Aplikacji Bitwarden. Więcej informacji o używaniu Rozszerzenia Aplikacji Bitwarden znajdziesz w widoku "Narzędzia". - - - Automatyczne wypełnianie - - - Czy chcesz automatycznie wypełnić czy wyświetlić ten element? - - - Czy na pewno chcesz automatycznie wypełnić tym elementem? Nie jest on w pełni dopasowany do "{0}". - - - Pasujące elementy - - - Prawdopodobnie pasujące elementy - - - Wyszukaj - - - Wyszukujesz element automatycznie wypełniany dla "{0}". - - - Udostępnij swój sejf - - - Utwórz organizację, aby bezpiecznie udostępniać innym użytkownikom swoje dane. - - - Skanuj, gdy aktywne jest pole hasła - - - Skanuj dane logowania i oferuj automatyczne wypełnianie tylko wtedy, gdy aktywne jest pole hasła. To ustawienie pozwala oszczędzać baterię. - - - Trwałe powiadomienie - - - Zawsze wyświetlaj powiadomienie automatycznego wypełniania i skanuj pola po próbie automatycznego wypełnienia. To ustawienie pomaga oszczędzać baterię. - - - Zawsze skanuj - - - Zawsze skanuj ekran w poszukiwaniu aktywnych pól i oferuj automatyczne wypełnianie tylko wtedy, gdy odnajdziesz pole hasła. To jest domyślne ustawienie. - - - Nie można otworzyć aplikacji "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplikacja uwierzytelniająca - For 2FA - - - Wprowadź 6-cyfrowy kod weryfikacyjny z Twojej aplikacji uwierzytelniającej. - For 2FA - - - Wprowadź 6-cyfrowy kod weryfikacyjny, który został przesłany do {0}. - For 2FA - - - Dane logowania niedostępne - For 2FA whenever there are no available providers on this device. - - - Dla tego konta aktywowano dwustopniowe logowanie, jednak to urządzenie nie wspiera żadnego ze skonfigurowanych mechanizmów dwustopniowej autoryzacji. Proszę użyć wspieranego urządzenia i/lub dodatkowych mechanizmów, które są lepiej obsługiwane przez różne urządzenia (np. aplikacja uwierzytelniająca). - - - Kod odzyskiwania - For 2FA - - - Zapamiętaj mnie - Remember my two-step login - - - Wyślij ponownie e-mail z kodem weryfikacyjnym - For 2FA - - - Opcje logowania dwustopniowego - - - Użyj innej metody logowania dwustopniowego - - - Nie można wysłać e-maila weryfikacyjnego. Spróbuj ponownie. - For 2FA - - - E-mail weryfikacyjny wysłany. - For 2FA - - - Przyłóż swój NEO YubiKey z tyłu urządzenia, aby kontynuować. - - - Klucz bezpieczeństwa YubiKey NEO - "YubiKey" is the product name and should not be translated. - - - Dodaj załącznik - - - Załączniki - - - Nie można pobrać pliku. - - - Twoje urządzenie nie może otworzyć pliku tego typu. - - - Pobieranie... - Message shown when downloading a file - - - Ten załącznik ma rozmiar {0}. Czy na pewno chcesz go pobrać na swoje urządzenie? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Klucz Uwierzytelniający (TOTP) - - - Kod weryfikacyjny (TOTP) - Totp code label - - - Klucz uwierzytelniający dodany. - - - Nie można odczytać klucza uwierzytelniającego. - - - Skanowanie rozpocznie się automatycznie. - - - Skieruj aparat w stronę kodu QR. - - - Zeskanuj kod QR - - - Aparat - - - Zdjęcia - - - Skopiowane TOTP! - - - Skopiuj TOTP - - - Jeśli Twoje dane logowania mają dołączony do nich klucz uwierzytelniający, kod weryfikacyjny TOTP jest automatycznie kopiowany do schowka przy każdym automatycznym wypełnianiu danych logowania. - - - Wyłącz automatyczne kopiowanie TOTP - - - Musisz posiadać konto Premium, aby skorzystać z tej funkcji. - - - Załącznik dodany - - - Załącznik usunięty - - - Wybierz plik - - - Plik - - - Nie wybrano pliku - - - Brak załączników. - - - Źródło pliku - - - Funkcja niedostępna - - - Maksymalny rozmiar pliku to 100 MB. - - - Nie możesz używać tej funkcji, dopóki nie zaktualizujesz klucza szyfrowania. - - - Dowiedz się więcej - - - Adres URL serwera API - - - Niestandardowe środowisko - - - Dla zaawansowanych użytkowników. Możesz określić podstawowy adres URL niezależnie dla każdej usługi. - - - URLe środowiska zostały zapisane. - - - {0} nie jest poprawnie sformatowany. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL serwera tożsamości - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Samodzielnie hostowane środowisko - - - Podaj podstawowy adres URL hostowanej przez Ciebie instalacji Bitwarden. - - - URL serwera - - - Adres URL serwera sejfu internetowego - - - Dotknij to powiadomienie, aby wyświetlić dane z sejfu. - - - Pola niestandardowe - - - Skopiuj numer - - - Skopiuj kod CVV/CVC - - - Numer - - - Kod zabezpieczający - - - Jakiego rodzaju element chcesz dodać? - - - Karta - - - Tożsamość - - - Dane logowania - - - Bezpieczna notatka - - - Adres 1 - - - Adres 2 - - - Adres 3 - - - Kwiecień - - - Sierpień - - - Marka - - - Imię i nazwisko posiadacza karty - - - Miasto - - - Firma - - - Kraj - - - Grudzień - - - Dr - - - Miesiąc wygaśnięcia - - - Rok wygaśnięcia - - - Luty - - - Imię - - - Styczeń - - - Lipiec - - - Czerwiec - - - Nazwisko - - - Numer prawa jazdy - - - Marzec - - - Maj - - - Drugie imię - - - Pan - - - Pani - - - Pani - - - Listopad - - - Październik - - - Numer paszportu - - - Telefon - - - September - - - PESEL - - - Województwo - - - Tytuł - - - Kod pocztowy - - - Adres - - - Wygaśnięcie - - - Wyłącz ikony stron - - - Możesz wyświetlać rozpoznawalną ikonę serwisu obok danych logowania w Twoim sejfie. - - - URL serwera ikon - - - Wypełnij pola automatycznie - - - Sejf jest zablokowany - - - Przejdź do mojego sejfu - - - Kolekcje - - - Brak elementów w tej kolekcji. - - - Brak elementów w tym folderze. - - - Autouzupełnienie – ułatwienia dostępu - - - Usługa Autouzupełniania Bitwarden wykorzystuje Androidowy Framework by pomóc wypełniać dane logowania, kart kredytowych i osobistych informacji w aplikacjach na twoim urządzeniu. - - - Użyj usługi autouzupełniania Bitwarden żeby wypełnić pola logowania, dane kart kredytowych i informacje osobiste w innych aplikacjach. - - - Otwórz ustawienia autouzupełniania - - - Face ID - What Apple calls their facial recognition reader. - - - Użyj Face ID do weryfikacji. - - - Użyj Face ID do odblokowania - - - Zweryfikuj Face ID - - - Odblokuj za pomocą Windows Hello - - - Zweryfikuj za pomocą Windows Hello - - - Funkcja Windows Hello - - - Nie udało nam się otworzyć menu ustawień usługi autouzupełniania Android. Możesz przejść do tego menu ręcznie, poprzez Ustawienia Android > System > Język i wprowadzanie > Zaawansowane > Usługa autouzupełniania. - - - Nazwa pola niestandardowego - - - Wartość logiczna - - - Pole maskowane - - - Tekst - - - Nowe pole niestandardowe - - - Jakiego rodzaju niestandardowe pole chcesz dodać? - - - Usuń - - - Nowy URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domena nadrzędna - - - Domyślne - - - Dokładnie - - - Nazwa domeny - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Wyrażenie regularne - A programming term, also known as 'RegEx'. - - - Rozpoczyna się od - - - Wykrywanie dopasowania URI - - - Wykrywanie dopasowania - URI match detection for auto-fill. - - - Tak i Zapisz - - - Automatycznie wypełnij i zapisz - - - Organizacja - An entity of multiple related people (ex. a team or business organization). - - - Przytrzymaj swój Yubikey w okolicy górnej części urządzenia. - - - Spróbuj ponownie - - - Aby kontynuować, przyłóż swój YubiKey NEO do tylnej części urządzenia. - - - Usługi ułatwień dostępu mogą być pomocne w sytuacji, gdy aplikacje nie obsługują standardowej usługi automatycznego uzupełniania. - - - Hasło zaktualizowane - ex. Date this password was updated - - - Zaktualizowano - ex. Date this item was updated - - - Autouzupełnianie aktywowane! - - - Musisz zalogować się do aplikacji głównej Bitwarden, zanim będziesz mógł używać autouzupełniania. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - Zalecamy wyłączenie w Ustawieniach innych aplikacji autouzupełniania, jeśli nie zamierzasz ich używać. - - - Uzyskaj dostęp do sejfu bezpośrednio z klawiatury, aby szybko i automatycznie uzupełniać hasła. - - - Aby włączyć automatyczne uzupełnianie haseł na swoim urządzeniu, wykonaj następujące czynności: - - - 1. przejdź do Ustawień systemu iOS - - - 2. Dotknij "Hasła & konta" - - - 3. Dotknij opcji "Autowypełnianie haseł" - - - 4. Włącz Autouzupełnianie - - - 5. Wybierz "Bitwarden" - - - Autouzupełnianie haseł - - - Najprostszym sposobem dodania nowych danych logowania do sejfu jest rozszerzenie Autouzupełniania Haseł Bitwarden. Więcej informacji o używaniu rozszerzenia Autouzupełniania Haseł Bitwarden znajdziesz w widoku "Narzędzia". - - diff --git a/src/App/Resources/AppResources.pt-BR.Designer.cs b/src/App/Resources/AppResources.pt-BR.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.pt-BR.resx b/src/App/Resources/AppResources.pt-BR.resx deleted file mode 100644 index ea24a6f7c..000000000 --- a/src/App/Resources/AppResources.pt-BR.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Sobre - - - Adicionar - Add/create a new entity (verb). - - - Adicionar Pasta - - - Adicionar Item - The title for the add item page. - - - Ocorreu um erro. - Alert title when something goes wrong. - - - Voltar - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Cancelar - Cancel an operation. - - - Copiar - Copy some value to your clipboard. - - - Copiar Senha - The button text that allows a user to copy the login's password to their clipboard. - - - Copiar o Nome de Usuário - The button text that allows a user to copy the login's username to their clipboard. - - - Créditos - Title for page that we use to give credit to resources that we use. - - - Excluir - Delete an entity (verb). - - - Excluindo... - Message shown when interacting with the server - - - Você realmente quer excluir? Isto não pode ser desfeito. - Confirmation alert message when deleteing something. - - - Editar - - - Editar Pasta - - - E-Mail - Short label for an email address. - - - Endereço de E-mail - Full label for a email address. - - - Envie-nos um E-mail - - - Envie-nos um e-mail diretamente para obter ajuda ou deixar feedback. - - - Digite o seu código PIN. - - - Favoritos - Title for your favorite items in the vault. - - - Enviar um Relatório de Bug - - - Abrir um issue no nosso repositório do GitHub. - - - Utilize a sua impressão digital para verificar. - - - Pasta - Label for a folder. - - - Nova pasta criada. - - - Pasta excluída. - - - Nenhuma Pasta - Items that have no folder specified go in this special "catch-all" folder. - - - Pastas - - - Pasta atualizada. - - - Ir para o Site - The button text that allows user to launch the website to their web browser. - - - Ajuda e Feedback - - - Ocultar - Hide a secret value that is currently shown (password). - - - Por favor, conecte-se à internet antes de continuar. - Description message for the alert when internet connection is required to continue. - - - É necessária uma conexão com a Internet - Title for the alert when internet connection is required to continue. - - - Senha Mestra Inválida. Tente novamente. - - - PIN inválido. Tente novamente. - - - Abrir - The button text that allows user to launch the website to their web browser. - - - Iniciar Sessão - The login button text (verb). - - - Login - Title for login page. (noun) - - - Encerrar Sessão - The log out button text (verb). - - - Tem certeza que deseja sair? - - - Senha Mestra - Label for a master password. - - - Mais - Text to define that there are more options things to see. - - - Meu Cofre - The title for the vault page. - - - Nome - Label for an entity name. - - - Não - - - Notas - Label for notes. - - - Ok - Acknowledgement. - - - Senha - Label for a password. - - - Salvar - Button text for a save operation (verb). - - - Salvando... - Message shown when interacting with the server - - - Configurações - The title for the settings page. - - - Mostrar - Reveal a hidden value (password). - - - O item foi excluído. - Confirmation message after successfully deleting a login. - - - Enviar - - - Sincronizar - The title for the sync page. - - - Agradecimentos - - - Ferramentas - The title for the tools page. - - - URL - Label for a uri/url. - - - Usar a impressão digital para desbloquear - - - Nome de Usuário - Label for a username. - - - O campo {0} é necessário. - Validation message for when a form field is left blank and is required to be entered. - - - {0} foi copiado. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verificar impressão digital - - - Verificar a Senha Mestra - - - Verificar PIN - - - Versão - - - Visualizar - - - Visitar o Nosso Site - - - Visite o nosso site para obter ajuda, notícias, enviar-nos um e-mail e/ou aprender mais sobre como utilizar o Bitwarden. - - - Site - Label for a website. - - - Sim - - - Conta - - - Sua nova conta foi criada! Agora você pode iniciar sessão. - - - Adicionar um Item - - - Extensão do Aplicativo - - - Utilize o serviço de acessibilidade do Bitwarden para autopreencher as suas credenciais entre aplicativos e a web. - - - Serviço de Autopreenchimento - - - Evitar Caracteres Ambíguos - - - Extensão do Aplicativo Bitwarden - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é a partir da extensão Bitwarden. Saiba mais sobre como utilizar a extensão Bitwarden navegando para a tela de "Ferramentas". - - - Use o Bitwarden no Safari e em outros aplicativos para preencher automaticamente suas credenciais. - - - Serviço de Autopreenchimento do Bitwarden - - - Utilize o serviço de acessibilidade Bitwarden para autopreencher as suas credenciais. - - - Alterar E-mail - - - Você pode alterar o seu endereço de e-mail no cofre web em bitwarden.com. Você deseja visitar o site agora? - - - Alterar Senha Mestra - - - Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora? - - - Fechar - - - Em Breve! - - - Continuar - - - Copiado! - - - Senha copiada! - - - Nome de usuário copiado! - - - Criar Conta - - - Criando conta... - Message shown when interacting with the server - - - Editar Item - - - Ativar Sincronização Automática - - - Digite o endereço de e-mail da sua conta para receber a dica da sua senha mestra. - - - Reativar Extensão do Aplicativo - - - Quase pronto! - - - Ativar Extensão do Aplicativo - - - No Safari, encontre o Bitwarden usando o ícone de compartilhamento (dica: role para a direita na linha inferior do menu). - Safari is the name of apple's web browser - - - Obtenha acesso instantâneo as suas senhas! - - - Você está pronto para iniciar sessão! - - - Ver Aplicativos Suportados - - - As suas credenciais estão agora facilmente acessíveis no Safari, Chrome, e outros aplicativos suportados. - - - No Safari e Chrome, encontre o Bitwarden usando o ícone de compartilhamento (dica: role para a direita na linha inferior do menu de compartilhamento). - - - Toque no ícone do Bitwarden no menu para iniciar a extensão. - - - Para ativar o Bitwarden no Safari e outros aplicativos, toque no ícone "Mais" no menu inferior. - - - Favorito - - - Impressão digital - - - Gerar Senha - - - Obter sua dica de senha mestra - - - Importar Itens - - - Você pode importar em massa os seus itens a partir do cofre web em bitwarden.com. Você deseja visitar o site agora? - - - Importe rapidamente em massa os seus itens de outros aplicativos de gerenciamento de senhas. - - - Última Sincronização: - - - Comprimento - - - Bloquear - - - 15 minutos - - - 1 hora - - - 1 minuto - - - 4 horas - - - Imediatamente - - - Opções de Bloqueio - - - Iniciando Sessão... - Message shown when interacting with the server - - - Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro. - - - Gerenciar - - - A confirmação de senha não está correta. - - - A senha mestra é a senha que você usa para acessar o seu cofre. É muito importante que você não esqueça sua senha mestra. Não há nenhuma maneira de recuperar a senha caso você se esqueça. - - - Dica de Senha Mestra (opcional) - - - Uma dica de senha mestra pode ajudá-lo(a) a lembrá-la caso você a esqueça. - - - A senha mestra deve ser pelo menos 8 caracteres. - - - Números Mínimos - Minimum numeric characters for password generator settings - - - Especiais Minímos - Minimum special characters for password generator settings - - - Mais Configurações - - - Você deve entrar no aplicativo Bitwarden antes de poder usar a extensão. - - - Nunca - - - Novo item criado. - - - Não existem favoritos no seu cofre. - - - Não existem itens no seu cofre. - - - Não existem itens no seu cofre para este site. Toque para adicionar um. - - - Esta credencial não tem um nome de usuário ou senha configurados. - - - Ok, entendi! - Confirmation, like "Ok, I understand it" - - - As opções padrões são definidas a partir da ferramenta de geração de senha do aplicativo Bitwarden. - - - Opções - - - Outros - - - Senha gerada. - - - Gerador de Senha - - - Dica de Senha - - - Enviamos um e-mail com a sua dica de senha mestra para você. - - - Tem certeza que deseja substituir a senha atual? - - - O Bitwarden mantém o seu cofre sincronizado automaticamente utilizando notificações push. Para a melhor experiência possível, por favor selecione "Permitir" na solicitação seguinte quando for perguntado para ativar as notificações push. - Push notifications for apple products - - - Avalie o Aplicativo - - - Por favor considere ajudar-nos com uma boa avaliação! - - - As avaliações da App Store são redefinidas a cada nova versão do Bitwarden. Por favor considere ajudar-nos com uma boa análise! - - - Gerar Nova Senha - - - Digite novamente a Senha Mestra - - - Pesquisar Cofre - - - Segurança - - - Ver o Progresso do Desenvolvimento - - - Selecionar - - - Definir PIN - - - Digite um código PIN de 4 dígitos para desbloquear o aplicativo. - - - Informação do Item - - - Item atualizado. - - - Enviando... - Message shown when interacting with the server - - - Sincronizando... - Message shown when interacting with the server - - - Sincronização completa. - - - A sincronização falhou. - - - Sincronizar Cofre Agora - - - Touch ID - What Apple calls their fingerprint reader. - - - Login em Duas Etapas - - - O login em duas etapas torna a sua conta mais segura ao exigir que você verifique seu login com outro dispositivo como uma chave de segurança, aplicativo de autenticação, SMS, ligação telefônica, ou e-mail. O login em duas etapas pode ser ativado no cofre web em bitwarden.com. Você deseja visitar o site agora? - - - Desbloquear com {0} - - - Desbloquear com Código PIN - - - Validando - Message shown when interacting with the server - - - Código de Verificação - - - Ver Item - - - Cofre Web do Bitwarden - - - Gerencie seus itens a partir de qualquer navegador da web com o cofre web do Bitwarden. - - - Perdeu o aplicativo autenticador? - - - Itens - Screen title - - - Extensão Ativada! - - - Ícones - - - Traduções - - - Itens para {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Não existem itens no seu cofre para {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Quando você ver uma notificação de autopreenchimento do Bitwarden, você pode tocá-la para iniciar o serviço de autopreenchimento. - - - Toque nesta notificação para autopreencher um item a partir do seu cofre. - - - Abrir as Configurações de Acessibilidade - - - 1. Na tela de Configurações de Acessibilidade do Android, toque em "Bitwarden" debaixo do cabeçalho Serviços. - - - 2. Ligue o interruptor e pressione OK para aceitar. - - - Desativado - - - Ativado - - - Estado - - - Beta - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é a partir do serviço de autopreenchimento do Bitwarden. Saiba mais sobre o serviço de autopreenchimento do Bitwarden ao navegar para a tela de "Ferramentas". - - - Autopreenchimento - - - Você pretende autopreencher ou ver esta credencial? - - - Tem certeza de que deseja preencher automaticamente este item? Ele não é uma correspondência completa para "{0}". - - - Itens Correspondentes - - - Itens Correspondentes Possíveis - - - Pesquisar - - - Você está procurando uma credencial de autopreenchimento para "{0}". - - - Compartilhar Seu Cofre - - - Crie uma organização para compartilhar com segurança suas credenciais com outros usuários. - - - Escanear quando o campo da senha estiver focado - - - Apenas escanear a tela por campos e oferecer uma notificação de autopreenchimento quando você selecionar um campo de senha. Esta configuração pode ajudar a conservar o tempo de vida útil da bateria. - - - Persistir a Notificação - - - Sempre oferecer uma notificação de autopreenchimento e apenas escanear por campos após tentar um autopreenchimento. Esta configuração pode ajudar a conservar o tempo de vida útil da bateria. - - - Sempre Escanear - - - Sempre escanear a tela por campos e oferecer autopreenchimento apenas se um campo de senha for encontrado. Esta é a configuração padrão. - - - Não é possível abrir o aplicativo "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplicativo de Autenticação - For 2FA - - - Digite o código de verificação de 6 dígitos do seu aplicativo de autenticação. - For 2FA - - - Digite o código de verificação de 6 dígitos que foi enviado por e-mail para {0}. - For 2FA - - - Sessão Indisponível - For 2FA whenever there are no available providers on this device. - - - Esta conta tem a verificação de duas etapas ativada, no entanto, nenhum dos provedores de verificação de duas etapas configurados são suportados neste dispositivo. Por favor, utilize um dispositivo suportado e/ou adicione provedores adicionais que são melhor suportados entre dispositivos (como um aplicativo de autenticação). - - - Código de Recuperação - For 2FA - - - Lembrar de mim - Remember my two-step login - - - Enviar e-mail do código de verificação novamente - For 2FA - - - Opções de Login em Duas Etapas - - - Usar outro método de login em duas etapas - - - Não foi possível enviar o e-mail de verificação. Tente novamente. - For 2FA - - - E-mail de verificação enviado. - For 2FA - - - Para continuar, segure a sua YubiKey NEO contra a parte de trás do seu dispositivo ou introduza a sua YubiKey na porta USB do seu dispositivo, depois toque no seu botão. - - - Chave de Segurança YubiKey NEO - "YubiKey" is the product name and should not be translated. - - - Adicionar Novo Anexo - - - Anexos - - - Não foi possível baixar o arquivo. - - - O seu dispositivo não pode abrir este tipo de arquivo. - - - Baixando... - Message shown when downloading a file - - - Este anexo tem {0} de tamanho. Tem certeza que deseja baixar para o seu dispositivo? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Chave de Autenticação (TOTP) - - - Código de Verificação (TOTP) - Totp code label - - - Chave do autenticador adicionada. - - - Não é possível ler a chave do autenticador. - - - O escaneamento acontecerá automaticamente. - - - Aponte sua câmera para o QR code. - - - Escanear Código QR - - - Câmera - - - Fotos - - - TOTP copiado! - - - Copiar TOTP - - - Se a sua credencial tiver uma chave de autenticação anexada, o código de verificação TOTP será copiado automaticamente para a área de transferência quando preencher automaticamente a credencial. - - - Desativar Cópia Automática de TOTP - - - Uma conta premium é necessária para usar esse recurso. - - - Anexo adicionado - - - Anexo excluído - - - Escolher Arquivo - - - Arquivo - - - Nenhum arquivo escolhido - - - Não existem anexos. - - - Fonte do Arquivo - - - Funcionalidade Indisponível - - - O tamanho máximo do arquivo é de 100 MB. - - - Você não pode usar esse recurso, até você atualizar sua chave de criptografia. - - - Saiba Mais - - - URL do Servidor da API - - - Ambiente Personalizado - - - Para usuários avançados. Você pode especificar a URL de base de cada serviço independentemente. - - - As URLs do ambiente foram salvas. - - - {0} não está formatado corretamente. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL do Servidor de Identidade - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Ambiente Auto-hospedado - - - Especifique a URL de base da sua instalação local do Bitwarden. - - - URL do Servidor - - - URL do Servidor do Cofre Web - - - Toque nesta notificação para visualizar as credenciais do seu cofre. - - - Campos Personalizados - - - Copiar Número - - - Copiar Código de Segurança - - - Número - - - Código de Segurança - - - Que tipo de item você pretende adicionar? - - - Cartão - - - Identidade - - - Credencial - - - Nota Segura - - - Endereço 1 - - - Endereço 2 - - - Endereço 3 - - - Abril - - - Agosto - - - Bandeira - - - Titular do Cartão - - - Cidade / Localidade - - - Empresa - - - País - - - Dezembro - - - Dr - - - Mês de Vencimento - - - Ano de Vencimento - - - Fevereiro - - - Primeiro Nome - - - Janeiro - - - Julho - - - Junho - - - Último Nome - - - Número da Licença - - - Março - - - Maio - - - Nome do Meio - - - Sr - - - Sra - - - Sra - - - Novembro - - - Outubro - - - Número de Passaporte - - - Telefone - - - Setembro - - - Cadastro de Pessoas Físicas (CPF) - - - Estado / Província - - - Título - - - CEP / Código Postal - - - Endereço - - - Vencimento - - - Desativar Ícones de Sites - - - O ícone do site fornece uma imagem reconhecível próximo de cada item de credencial no seu cofre. - - - URL do Servidor de Ícones - - - Autopreencher com o Bitwarden - - - O cofre está trancado. - - - Ir para o meu cofre - - - Coleções - - - Não existem itens nesta coleção. - - - Não existem itens nesta pasta. - - - Serviço de Acessibilidade de Autopreenchimento - - - O serviço de autopreenchimento do Bitwarden utiliza a Estrutura de Autopreenchimento do Android para ajudar no preenchimento de credenciais, cartões de crédito, e informação de identidade em outros aplicativos do seu dispositivo. - - - Utilize o serviço de autopreenchimento do Bitwarden para preencher automaticamente suas credenciais, cartões de crédito e informação de identidade em outros aplicativos. - - - Abrir Configurações de Autopreenchimento - - - Face ID - What Apple calls their facial recognition reader. - - - Usar o Face ID para verificar. - - - Usar o Face ID para Desbloquear - - - Verificar o Face ID - - - Desbloquear com o Windows Hello - - - Verificar com o Windows Hello - - - Windows Hello - - - Não foi possível abrir automaticamente o menu de configurações de autopreenchimento do Android para você. Você pode navegar para o menu de configurações de autopreenchimento manualmente a partir de Configurações do Android > Sistema > Idiomas e Entrada > Avançado > Serviço de autopreenchimento. - - - Nome do campo personalizado - - - Booleano - - - Ocultado - - - Texto - - - Novo Campo Personalizado - - - Que tipo de campo personalizado você deseja adicionar? - - - Remover - - - Novo URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domínio de base - - - Padrão - - - Exato - - - Servidor - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Expressão regular - A programming term, also known as 'RegEx'. - - - Começa com - - - Correspondência de Detecção de URI - - - Detecção de Correspondência - URI match detection for auto-fill. - - - Sim, e Salvar - - - Autopreencher e salvar - - - Organização - An entity of multiple related people (ex. a team or business organization). - - - Segure a sua Yubikey perto do topo do dispositivo. - - - Tentar novamente - - - Para continuar, segure a sua YubiKey NEO contra a parte de trás do dispositivo. - - - O serviço de acessibilidade pode ser útil para se usar quando os aplicativos não suportam o serviço de autopreenchimento padrão. - - - Senha Atualizada - ex. Date this password was updated - - - Atualizado - ex. Date this item was updated - - - Autopreenchimento Ativado! - - - Você deve iniciar a sessão no aplicativo principal do Bitwarden, antes de poder usar o Autopreenchimento. - - - Suas credenciais agora são facilmente acessíveis diretamente do seu teclado, enquanto você faz login em aplicativos e sites. - - - Recomendamos desabilitar qualquer outro aplicativo de autopreenchimento em Ajustes, se você não pretende usá-los. - - - Acesse seu cofre diretamente do seu teclado para autopreencher as senhas rapidamente. - - - Para ativar o autopreenchimento de senha no seu dispositivo, siga estas instruções: - - - 1. Acesse o aplicativo "Ajustes" do iOS - - - 2. Toque em "Senhas e Contas" - - - 3. Toque em "Preencher Senhas" - - - 4. Ative o Autopreenchimento - - - 5. Selecione "Bitwarden" - - - Autopreenchimento de Senha - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é usando a extensão Autopreencher do Bitwarden. Saiba mais sobre como usar a extensão Autopreencher do Bitwarden navegando até a tela "Ferramentas". - - diff --git a/src/App/Resources/AppResources.pt-PT.Designer.cs b/src/App/Resources/AppResources.pt-PT.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.pt-PT.resx b/src/App/Resources/AppResources.pt-PT.resx deleted file mode 100644 index 21392cf1f..000000000 --- a/src/App/Resources/AppResources.pt-PT.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Acerca - - - Adicionar - Add/create a new entity (verb). - - - Adicionar pasta - - - Adicionar item - The title for the add item page. - - - Ocorreu um erro. - Alert title when something goes wrong. - - - Retroceder - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Cancelar - Cancel an operation. - - - Copiar - Copy some value to your clipboard. - - - Copiar palavra-passe - The button text that allows a user to copy the login's password to their clipboard. - - - Copiar nome de utilizador - The button text that allows a user to copy the login's username to their clipboard. - - - Créditos - Title for page that we use to give credit to resources that we use. - - - Eliminar - Delete an entity (verb). - - - A eliminar... - Message shown when interacting with the server - - - Pretende mesmo eliminar? Isto não pode ser desfeito. - Confirmation alert message when deleteing something. - - - Editar - - - Editar pasta - - - Email - Short label for an email address. - - - Endereço de email - Full label for a email address. - - - Enviar-nos um email - - - Envie-nos um email diretamente para obter ajuda ou deixar feedback. - - - Introduza o seu código PIN. - - - Favoritos - Title for your favorite items in the vault. - - - Submeter um relatório de bug - - - Abrir um issue no nosso repositório do GitHub. - - - Utilize a sua impressão digital para verificar. - - - Pasta - Label for a folder. - - - Nova pasta criada. - - - Pasta eliminada. - - - Nenhuma pasta - Items that have no folder specified go in this special "catch-all" folder. - - - Pastas - - - Pasta atualizada. - - - Ir para o website - The button text that allows user to launch the website to their web browser. - - - Ajuda e feedback - - - Ocultar - Hide a secret value that is currently shown (password). - - - Por favor ligue-se à internet antes de continuar. - Description message for the alert when internet connection is required to continue. - - - Ligação à internet requerida - Title for the alert when internet connection is required to continue. - - - Palavra-passe mestra inválida. Tente novamente. - - - PIN inválido. Tente novamente. - - - Iniciar - The button text that allows user to launch the website to their web browser. - - - Iniciar sessão - The login button text (verb). - - - Iniciar sessão - Title for login page. (noun) - - - Terminar sessão - The log out button text (verb). - - - Tem a certeza de que pretende terminar sessão? - - - Palavra-passe mestra - Label for a master password. - - - Mais - Text to define that there are more options things to see. - - - Meu cofre - The title for the vault page. - - - Nome - Label for an entity name. - - - Não - - - Notas - Label for notes. - - - Ok - Acknowledgement. - - - Palavra-passe - Label for a password. - - - Guardar - Button text for a save operation (verb). - - - A guardar... - Message shown when interacting with the server - - - Definições - The title for the settings page. - - - Mostrar - Reveal a hidden value (password). - - - O item foi eliminado. - Confirmation message after successfully deleting a login. - - - Submeter - - - Sincronizar - The title for the sync page. - - - Obrigado - - - Ferramentas - The title for the tools page. - - - URI - Label for a uri/url. - - - Utilizar impressão digital para desbloquear - - - Nome de utilizador - Label for a username. - - - O campo {0} é requerido. - Validation message for when a form field is left blank and is required to be entered. - - - {0} foi copiado. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verificar impressão digital - - - Verificar palavra-passe mestra - - - Verifica PIN - - - Versão - - - Ver - - - Visitar o nosso website - - - Visite o nosso website para obter ajuda, notícias, enviar-nos um email, e/ou saber mais acerca de como utilizar o Bitwarden. - - - Website - Label for a website. - - - Sim - - - Conta - - - A sua nova conta foi criada! Agora pode iniciar sessão. - - - Adicionar um item - - - Extensão da aplicação - - - Utilize o serviço de acessibilidade do Bitwarden para auto-preencher as suas credencias entre aplicações e a web. - - - Serviço de auto-preenchimento - - - Evitar caracteres ambíguos - - - Extensão da aplicação Bitwarden - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é a partir da extensão da aplicação Bitwarden. Saiba mais acerca de como utilizar a extensão da aplicação Bitwarden ao navegar para o ecrã de "Ferramentas". - - - Utilize o Bitwarden no Safari e outras aplicações para auto-preencher as suas credenciais. - - - Serviço de auto-preenchimento Bitwarden - - - Utilize o serviço de acessibilidade do Bitwarden para auto-preencher as suas credencias. - - - Alterar email - - - Pode alterar o seu endereço de email no cofre web bitwarden.com. Pretende visitar o website agora? - - - Alterar palavra-passe mestra - - - Pode alterar a sua palavra-passe mestra no cofre web bitwarden.com. Pretende visitar o website agora? - - - Fechar - - - Brevemente! - - - Continuar - - - Copiado! - - - Palavra-passe copiada! - - - Nome de utilizador copiado! - - - Criar conta - - - A criar conta... - Message shown when interacting with the server - - - Editar item - - - Ativar sincronização automática - - - Introduza o endereço de email da sua conta para receber a dica da sua palavra-passe mestra. - - - Reativar extensão da aplicação - - - Quase feito! - - - Ativar extensão da aplicação - - - No Safari, encontre o Bitwarden utilizando o ícone de partilha (dica: role para a direita na linha de fundo do menu de partilha). - Safari is the name of apple's web browser - - - Obtenha acesso instantâneo às suas palavras-passe! - - - Está pronto para iniciar sessão! - - - Ver aplicações suportadas - - - As suas credenciais estão agora facilmente acessíveis a partir do Safari, Chrome, e outras aplicações suportadas. - - - No Safari e Chrome, encontre o Bitwarden utilizando o ícone de partilha (dica: desloque para a direita na linha de fundo do menu de partilha). - - - Toque no ícone do Bitwarden no menu para iniciar a extensão. - - - Para ligar o Bitwarden no Safari e outras aplicações, toque no ícone "mais" na linha de fundo do menu. - - - Favorito - - - Impressão digital - - - Gerar palavra-passe - - - Obter dica da palavra-passe mestra - - - Importar itens - - - Pode importar em massa os seus itens a partir do cofre web bitwarden.com. Pretende visitar o website agora? - - - Importe rapidamente em massa os seus itens de outras aplicações de gestão de palavras-passe. - - - Última sincronização: - - - Comprimento - - - Bloquear - - - 15 minutos - - - 1 hora - - - 1 minuto - - - 4 horas - - - Imediatamente - - - Opções de bloqueio - - - A iniciar sessão... - Message shown when interacting with the server - - - Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro. - - - Gerir - - - A confirmação da palavra-passe não está correta. - - - A palavra-passe mestra é a palavra-passe que utiliza para aceder ao seu cofre. É muito importante que não se esqueça da sua palavra-passe mestra. Não existe maneira de recuperar a palavra-passe no caso de a esquecer. - - - Dica da palavra-passe mestra (opcional) - - - Uma dica da palavra-passe mestra pode ajudar a lembrar-se da sua palavra-passe se a esquecer. - - - A palavra-passe mestra tem de ter pelo menos 8 caracteres. - - - Números mínimos - Minimum numeric characters for password generator settings - - - Especiais mínimos - Minimum special characters for password generator settings - - - Mais definições - - - Tem de iniciar sessão na aplicação Bitwarden principal antes de poder utilizar a extensão. - - - Nunca - - - Novo item criado. - - - Não existem favoritos no seu cofre. - - - Não existem itens no seu cofre. - - - Não existem itens no seu cofre para este website/aplicação. Toque para adicionar um. - - - Esta credencial não tem um nome de utilizador ou palavra passe configurados. - - - Ok, entendi! - Confirmation, like "Ok, I understand it" - - - Predefinições de opções são definidas a partir da ferramenta de geração de palavras-passe da aplicação principal Bitwarden. - - - Opções - - - Outros - - - Palavra-passe gerada. - - - Gerador de palavras-passe - - - Dica da palavra-passe - - - Enviámos-lhe um email com a dica da sua palavra-passe mestra. - - - Tem a certeza de que pretende sobreescrever a palavra-passe atual? - - - O Bitwarden mantém o seu cofre sincronizado automaticamente utilizando notificações push. Para a melhor experiência possível, por favor selecione "Ok" na seguinte solicitação quando solicitado a ativar notificações push. - Push notifications for apple products - - - Avaliar a aplicação - - - Por favor considere ajudar-nos com uma boa análise! - - - As avaliações da App Store são repostas a cada versão do Bitwarden. Por favor considere ajudar-nos com uma boa análise! - - - Regenerar palavra-passe - - - Reescreva a palavra-passe mestra - - - Pesquisar cofre - - - Segurança - - - Ver progresso do desenvolvimento - - - Selecionar - - - Definir PIN - - - Introduza um código PIN de 4 dígitos para desbloquear a aplicação. - - - Informação do item - - - Item atualizado. - - - A submeter... - Message shown when interacting with the server - - - A sincronizar... - Message shown when interacting with the server - - - Sincronização completada. - - - Sincronização falhada. - - - Sincronizar cofre agora - - - Touch ID - What Apple calls their fingerprint reader. - - - Início de sessão de dois passos - - - O início de sessão de dois passos torna a sua conta mais segura ao requerer que verifique o seu início de sessão com outro dispositivo como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica, ou email. O início de sessão de dois passos pode ser ativado no cofre web bitwarden.com. Pretende visitar o website agora? - - - Desbloquear com {0} - - - Desbloquear com código PIN - - - A validar - Message shown when interacting with the server - - - Código de verificação - - - Ver item - - - Cofre Web Bitwarden - - - Gira os seus itens a partir de qualquer navegador web com o cofre web Bitwarden. - - - Perdeu a aplicação do autenticador? - - - Itens - Screen title - - - Extensão ativada! - - - Ícones - - - Traduções - - - Itens para {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Não existem itens no seu cofre para {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Quando vê uma notificação de auto-preenchimento do Bitwarden, pode tocar na mesma para iniciar o serviço de auto-preenchimento. - - - Toque nesta notificação para auto-preencher um item do seu cofre. - - - Abrir definições de acessibilidade - - - 1. No ecrã de Definições de Acessibilidade do Android, toque em "Bitwarden" debaixo do cabeçalho Serviços. - - - 2. Ligue o interruptor e pressione OK para aceitar. - - - Desativado - - - Ativado - - - Estado - - - Beta - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é a partir do serviço de auto-preenchimento Bitwarden. Saiba mais acerca do serviço de auto-preenchimento Bitwarden ao navegar para o ecrã de "Ferramentas". - - - Auto-preencher - - - Pretende auto-preencher ou ver este item? - - - Tem a certeza de que pretende auto-preencher este item? Não é uma correspondência completa para "{0}". - - - Itens correspondentes - - - Possíveis itens correspondentes - - - Pesquisar - - - Está a pesquisar por um item de auto-preenchimento para "{0}". - - - Partilhar o seu cofre - - - Crie uma organização para partilhar os seus itens em segurança com outros utilizadores. - - - Scanear quando o campo de palavra-passe está focado - - - Apenas scanear o ecrã por campos e oferecer uma notificação de auto-preenchimento quando seleciona um campo de palavra-passe. Esta definição pode ajudar a conservar a vida da bateria. - - - Notificação persistente - - - Oferecer sempre uma notificação de auto-preenchimento e apenas scanear por campos após tentar um auto-preenchimento. Esta definição pode ajudar a conservar a vida da bateria. - - - Scanear sempre - - - Scanear sempre o ecrã por campos e apenas oferecer uma notificação de auto-preenchimento se forem encontrados campos de palavra-passe. Esta é a predefinição. - - - Não é possível abrir a aplicação "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplicação de autenticador - For 2FA - - - Introduza o código de verificação de 6 dígitos da sua aplicação de autenticador. - For 2FA - - - Introduza o código de verificação de 6 dígitos que foi enviado por email para {0}. - For 2FA - - - Início de sessão indisponível - For 2FA whenever there are no available providers on this device. - - - Esta conta tem início de sessão de dois passos ativado, no entanto, nenhum dos fornecedores de dois passos configurados são suportados neste dispositivo. Por favor utilize um dispositivo suportado e/ou adiciono fornecedores adicionais que são melhor suportados entre dispositivos (como uma aplicação de autenticador). - - - Código de recuperação - For 2FA - - - Memorizar-me - Remember my two-step login - - - Enviar código de verificação novamente - For 2FA - - - Opções de início de sessão de dois passos - - - Utilizar outro método de início de sessão de dois passos - - - Não foi possível enviar o email de verificação. Tente novamente. - For 2FA - - - Email de verificação enviado. - For 2FA - - - Para continuar, segure a sua YubiKey NEO contra a parte de trás do dispositivo ou introduza a sua YubiKey na porta USB do seu dispositivo, depois toque no seu botão. - - - Chave de segurança YubiKey - "YubiKey" is the product name and should not be translated. - - - Adicionar novo anexo - - - Anexos - - - Não foi possível descarregar o ficheiro. - - - O seu dispositivo não consegue abrir este tipo de ficheiro. - - - A descarregar... - Message shown when downloading a file - - - O anexo tem {0} de tamanho. Tem a certeza que deseja descarregá-lo para o seu dispositivo? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Chave de autenticador (TOTP) - - - Código de verificação (TOTP) - Totp code label - - - Chave de autenticador adicionada. - - - Não é possível ler a chave de autenticador. - - - A digitalização irá acontecer automaticamente. - - - Aponte a sua câmara ao código QR. - - - Digitalizar código QR - - - Câmara - - - Fotografias - - - TOTP copiado! - - - Copiar TOTP - - - Se a sua credencial tem uma chave de autenticador anexada à mesma, o código de verificação TOTP é copiado automaticamente para a sua área de transferência quando quer que auto-preencha a credencial. - - - Desativar cópia automática de TOTP - - - É requerida uma adesão premium para utilizar esta funcionalidade. - - - Anexo adicionado - - - Anexo eliminado - - - Escolher ficheiro - - - Ficheiro - - - Nenhum ficheiro escolhido - - - Não existem anexos. - - - Fonte de ficheiro - - - Funcionalidade indisponível - - - O tamanho máximo do ficheiro é de 100 MB. - - - Não pode utilizar esta funcionalidade até atualizar a sua chave de encriptação. - - - Saber mais - - - URL do servidor da API - - - Ambiente personalizado - - - Para utilizadores avançados. Pode especificar o URL de base de cada serviço independentemente. - - - Os URLs de ambiente foram guardados. - - - {0} não está formatado corretamente. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL do servidor de identidade - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Ambiente auto-hospedado - - - Especifique o URL de base da sua instalação local do Bitwarden. - - - URL do servidor - - - URL do servidor do cofre web - - - Toque nesta notificação para ver itens do seu cofre. - - - Campos personalizados - - - Copiar número - - - Copiar código de segurança - - - Número - - - Código de segurança - - - Que tipo de item pretende adicionar? - - - Cartão - - - Identidade - - - Credencial - - - Nota segura - - - Endereço 1 - - - Endereço 2 - - - Endereço 3 - - - Abril - - - Agosto - - - Marca - - - Nome do titular do cartão - - - Cidade / localidade - - - Empresa - - - País - - - Dezembro - - - Dr - - - Mês de expiração - - - Ano de expiração - - - Fevereiro - - - Primeiro nome - - - Janeiro - - - Julho - - - Junho - - - Último nome - - - Número da licença - - - Março - - - Maio - - - Nome do meio - - - Sr - - - Sra - - - Sra - - - Novembro - - - Outubro - - - Número do passaporte - - - Telefone - - - Setembro - - - Número de segurança social - - - Estado / província - - - Título - - - Código postal - - - Endereço - - - Expiração - - - Desativar ícones de websites - - - Os ícones de websites providenciam uma imagem reconhecível ao lado de cada item de credencial no seu cofre. - - - URL do servidor de ícones - - - Auto-preencher com o Bitwarden - - - O cofre está bloqueado - - - Ir para o meu cofre - - - Coleções - - - Não existem itens nesta coleção. - - - Não existem itens nesta pasta. - - - Serviço de acessibilidade de auto-preenchimento - - - O serviço de auto-preenchimento do Bitwarden utiliza a Android Autofill Framework para assistir a preencher credenciais, cartões de crédito, e informação de identidade noutras aplicações do seu dispositivo. - - - Utilize o serviço de auto-preenchimento do Bitwarden para preencher credenciais, cartões de crédito, e informação de identidade noutras aplicações. - - - Abrir definições de auto-preenchimento - - - Face ID - What Apple calls their facial recognition reader. - - - Utilizar o Face ID para verificar. - - - Utilizar o Face ID para desbloquear - - - Verificar Face ID - - - Desbloquear com o Windows Hello - - - Verificar com o Windows Hello - - - Windows Hello - - - Não conseguimos abrir automaticamente o menu de definições de auto-preenchimento do Android por si. Pode navegar até ao menu de definições de auto-preenchimento manualmente a partir de Definições do Android > Sistema > Idiomas e introdução > Avançado > Serviço de auto-preenchimento. - - - Nome do campo personalizado - - - Booleano - - - Ocultado - - - Texto - - - Novo campo personalizado - - - Que tipo de campo personalizado pretende adicionar? - - - Remover - - - Novo URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domínio base - - - Predefinição - - - Exato - - - Servidor - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Expressão regular - A programming term, also known as 'RegEx'. - - - Começa por - - - Deteção de correspondência de URI - - - Deteção de correspondência - URI match detection for auto-fill. - - - Sim, e guardar - - - Auto-preencher e guardar - - - Organização - An entity of multiple related people (ex. a team or business organization). - - - Segure a sua Yubikey perto do topo do dispositivo. - - - Tentar novamente - - - Para continuar, segure a sua YubiKey NEO contra a parte de trás do dispositivo. - - - O serviço de acessibilidade pode ser útil para utilizar quando as aplicações não suportam o serviço de auto-preenchimento padrão. - - - Palavra passe atualizada - ex. Date this password was updated - - - Atualizado - ex. Date this item was updated - - - Auto-preenchimento ativado! - - - Tem de iniciar sessão na aplicação Bitwarden principal antes de poder utilizar auto-preenchimento. - - - As suas credenciais são agora facilmente acessíveis a partir do seu teclado ao iniciar sessão em aplicações e websites. - - - Recomendamos desativar quaisquer outras aplicações de auto-preenchimento nas Definições se não as planeia utilizar. - - - Aceda ao seu cofre diretamente a partir do seu teclado para rapidamente auto-preencher palavras-passe. - - - Para ativar o auto-preenchimento de palavras-passe no seu dispositivo, siga estas instruções: - - - 1. Vá à aplicação iOS "Definições" - - - 2. Toque em "Palavras-passe e contas" - - - 3. Toque em "auto-preencher palavras-passe" - - - 4. Ligue o auto-preenchimento - - - 5. Selecione "Bitwarden" - - - Auto-preenchimento de palavras-passe - - - A maneira mais fácil de adicionar novas credenciais ao seu cofre é ao utilizar a extensão de auto-preenchimento de palavras-passe do Bitwarden. Saiba mais acerca de como utilizar a extensão de auto-preenchimento de palavras-passe do Bitwarden ao navegar para o ecrã de "Ferramentas". - - diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 2e8ef35d5..ac7702d09 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -656,7 +656,7 @@ Re-type Master Password - Search Vault + Search vault Security @@ -1346,4 +1346,167 @@ The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. + + Invalid email address. + + + Cards + + + Identities + + + Logins + + + Secure Notes + + + All Items + + + URIs + Plural form of a URI + + + Checking password... + A loading message when doing an exposed password check. + + + Check if password has been exposed. + + + This password has been exposed {0} time(s) in data breaches. You should change it. + + + This password was not found in any known data breaches. It should be safe to use. + + + Identity Name + + + Value + + + Password History + + + Types + + + No passwords to list. + + + There are no items to list. + + + Search collection + + + Search folder + + + Search type + + + Type + + + Move Down + + + Move Up + + + Miscellaneous + + + Ownership + + + Who owns this item? + + + There are no collections to list. + + + Item has been shared. + + + You must select at least one collection. + + + Share + + + Share Item + + + No organizations to list. + + + Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared. + + + Number of Words + + + Passphrase + + + Word Separator + + + Clear + To clear something out. example: To clear browser history. + + + Generator + Short for "Password Generator" + + + There are no folders to list. + + + Fingerprint Phrase + A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing. + + + Your account's fingerprint phrase + A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing. + + + Bitwarden allows you to share your vault with others by using an organization account. Would you like to visit the bitwarden.com website to learn more? + + + Export Vault + + + Lock Now + + + PIN + + + Unlock + + + 30 minutes + + + 5 minutes + + + Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application. + + + Logged in as {0}. + ex: Logged in as user@example.com. + + + Your vault is locked. Verify your master password to continue. + + + Your vault is locked. Verify your PIN code to continue. + \ No newline at end of file diff --git a/src/App/Resources/AppResources.ro.Designer.cs b/src/App/Resources/AppResources.ro.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.ro.resx b/src/App/Resources/AppResources.ro.resx deleted file mode 100644 index 880ea3cb4..000000000 --- a/src/App/Resources/AppResources.ro.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Despre - - - Adăugaţi - Add/create a new entity (verb). - - - Adăugare dosar - - - Adăugare articol - The title for the add item page. - - - S-a produs o eroare. - Alert title when something goes wrong. - - - Înapoi - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - Anulare - Cancel an operation. - - - Copiere - Copy some value to your clipboard. - - - Copiere parolă - The button text that allows a user to copy the login's password to their clipboard. - - - Copiere nume de utilizator - The button text that allows a user to copy the login's username to their clipboard. - - - Mulţumiri - Title for page that we use to give credit to resources that we use. - - - Ştergeţi - Delete an entity (verb). - - - Ştergere... - Message shown when interacting with the server - - - Într-adevăr doriţi să ştergeţi? Acest lucru nu poate fi anulat. - Confirmation alert message when deleteing something. - - - Editare - - - Editare dosar - - - E-mail - Short label for an email address. - - - Adresă de e-mail - Full label for a email address. - - - Trimiteţi-ne un e-mail - - - Trimiteţi-ne un e-mail pentru a obţine ajutor sau pentru a lăsa un feedback. - - - Introduceţi codul PIN. - - - Favorite - Title for your favorite items in the vault. - - - Raport de Bug - - - Deschideţi o problemă pe pagina noastră GitHub. - - - Utilizaţi amprenta dvs. pentru a verifica. - - - Dosar - Label for a folder. - - - A fost creat un dosar nou. - - - Dosar șters. - - - Niciun dosar - Items that have no folder specified go in this special "catch-all" folder. - - - Dosare - - - Dosar actualizat. - - - Accesaţi la site-ul web - The button text that allows user to launch the website to their web browser. - - - Ajutor si feedback - - - Ascundeți - Hide a secret value that is currently shown (password). - - - Vă rugăm să vă conectaţi la internet înainte de a continua. - Description message for the alert when internet connection is required to continue. - - - Conexiunea la internet este necesară - Title for the alert when internet connection is required to continue. - - - Parola principală este greşită. Încercaţi din nou. - - - PIN greşit. Încercaţi din nou. - - - Lansare - The button text that allows user to launch the website to their web browser. - - - Logare - The login button text (verb). - - - Autentificare - Title for login page. (noun) - - - Deconectare - The log out button text (verb). - - - Sunteți sigur că doriți să vă deconectați? - - - Parola principală - Label for a master password. - - - Mai multe - Text to define that there are more options things to see. - - - Seiful meu - The title for the vault page. - - - Nume - Label for an entity name. - - - Nu - - - Note - Label for notes. - - - Ok - Acknowledgement. - - - Parolă - Label for a password. - - - Salvare - Button text for a save operation (verb). - - - Salvare... - Message shown when interacting with the server - - - Setări - The title for the settings page. - - - Afişare - Reveal a hidden value (password). - - - Articolul a fost şters. - Confirmation message after successfully deleting a login. - - - Trimiteți - - - Sincronizare - The title for the sync page. - - - Mulţumim - - - Instrumente - The title for the tools page. - - - URI - Label for a uri/url. - - - Utilizaţi amprenta pentru a debloca - - - Nume de utilizator - Label for a username. - - - Câmpul {0} este necesar. - Validation message for when a form field is left blank and is required to be entered. - - - {0} a fost copiat. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verificaţi amprenta - - - Verificaţi parola principală - - - Verificaţi codul PIN - - - Versiune - - - Vizualizaţi - - - Vizitați-ne site-ul web - - - Vizitaţi site-ul nostru pentru a primi ajutor, ştiri, pentru a trimite e-mail, şi/sau aflaţi mai multe despre cum să utilizaţi Bitwarden. - - - Site web - Label for a website. - - - Da - - - Cont - - - Contul a fost creat! Acum vă puteţi autentifica. - - - Adăugare articol - - - Extensie aplicaţie - - - Utilizați serviciul de accesibilitate Bitwarden pentru a completa automat datele de autentificare între aplicații și pagini web. - - - Serviciul completare automată - - - Evitați caracterele ambigue - - - Extensia aplicaţiei Bitwarden - - - Cea mai ușoară modalitate de a adăuga noi intrări în seif este din extensia aplicaţiei Bitwarden. Aflați mai multe despre utilizarea extensiei aplicaţiei Bitwarden accesând ecranul "Instrumente". - - - Utilizați Bitwarden în Safari și în alte aplicații pentru a completa automat datele de autentificare. - - - Serviciul de completare automată Bitwarden - - - Utilizați serviciul Bitwarden pentru a completa automat datele de autentificare. - - - Schimbați adresa de e-mail - - - Puteţi modifica adresa de e-mail pe site-ul web bitwarden.com. Doriţi să vizitaţi site-ul acum? - - - Schimbaţi parola principală - - - Puteţi modifica parola principală pe site-ul web bitwarden.com. Doriţi să vizitaţi site-ul acum? - - - Închideţi - - - În curând! - - - Continuare - - - Copiat! - - - Parolă copiată! - - - Nume de utilizator copiat! - - - Creare cont - - - Se creează contul... - Message shown when interacting with the server - - - Editare articol - - - Activaţi sincronizarea automată - - - Introduceţi adresa de e-mail a contului pentru a primi indiciul parolei principale. - - - Reactivați extensia aplicației - - - Aproape gata! - - - Activați extensia aplicației - - - În Safari, găsiţi Bitwarden utilizând pictograma de partajare (indiciu: derulaţi la dreapta pe rândul de jos al meniului). - Safari is the name of apple's web browser - - - Obţineţi acces instantaneu la parolele dvs! - - - Sunteţi gata să vă autentificaţi! - - - Vedeţi aplicaţiile acceptate - - - Datele dvs. de conectare sunt acum uşor accesibile din Safari, Chrome şi alte aplicaţii suportate. - - - În Safari şi Chrome, găsiţi Bitwarden utilizând pictograma de partajare (indiciu: derulaţi la dreapta pe rândul de jos al meniului partajare). - - - Atingeţi pictograma Bitwarden din meniu pentru a lansa extensia. - - - Pentru a activa Bitwarden în Safari şi alte aplicaţii, atingeţi pictograma "mai multe" pe rândul de jos al meniului. - - - Favorit - - - Amprentă - - - Generare parolă - - - Obţineţi indiciul parolei principale - - - Import articole - - - Puteți să importați datele de conectare din seiful web bitwarden.com. Doriți să vizitați saitul acum? - - - Import rapid al datelor de conectare din alte aplicații de gestionare a parolelor. - - - Ultima sincronizare: - - - Lungime - - - Blocare - - - 15 minute - - - 1 oră - - - 1 minut - - - 4 ore - - - Imediat - - - Blocare - - - Autentificare... - Message shown when interacting with the server - - - Autentificaţi-vă sau creaţi un cont nou pentru a accesa seiful. - - - Gestionare - - - Confirmarea parolei nu este corectă. - - - Parola principală este parola pe care o utilizaţi pentru a accesa seiful dvs. Este foarte important să nu uitaţi parola principală. Nu există nici o modalitate de a recupera parola în cazul în care aţi uitat-o. - - - Indiciul pentru parola principală (opţional) - - - Un indiciu pentru parola principală vă poate ajuta să vă reamintiţi parola dacă aţi uitat-o. - - - Parola principală trebuie să fie de cel puțin 8 caractere. - - - Număr minim - Minimum numeric characters for password generator settings - - - Minim special - Minimum special characters for password generator settings - - - Mai multe setări - - - Tu trebuie să vă autentificaţi în aplicaţia principală Bitwarden înainte de a utiliza extensia. - - - Niciodată - - - A fost creat un nou articol. - - - Nu sunt favorite în seiful dvs. - - - Nu sunt articole în seiful dvs. - - - Nu sunt date de conectare în seif pentru acest sait. Atingeți pentru a adăuga un articol. - - - Această autentificare nu are un nume de utilizator sau parolă configurate. - - - Ok, am înţeles! - Confirmation, like "Ok, I understand it" - - - Opțiunile implicite sunt setate de instrumentul principal al generatorului de parole al aplicației Bitwarden. - - - Opțiuni - - - Altele - - - Parolă generată. - - - Generator de parole - - - Indiciu parolă - - - V-am trimis un e-mail cu indiciul parolei principale. - - - Sunteţi sigur că doriţi să suprascrieţi parola curentă? - - - Bitwarden păstrează seiful dvs. sincronizat utilizând notificările "push". Pentru cele mai bune rezultate posibile, selectați "Ok" la următorul mesaj când vi se solicită să activați notificările "push". - Push notifications for apple products - - - Evaluaţi aplicaţia - - - Vă rugăm să luați în considerare să ne ajutați cu o recenzie bună! - - - Statisticile App Store sunt resetate cu fiecare nouă versiune Bitwarden. Vă rugăm să luați în considerare să ne ajutați cu o recenzie bună! - - - Regenerare parolă - - - Reintroduceţi parola principală - - - Căutare în seif - - - Securitate - - - Vizualizaţi progresul de dezvoltare - - - Selectare - - - Setare PIN - - - Introduceţi un cod PIN din 4 cifre pentru a debloca aplicaţia. - - - Informații de autentificare - - - Articolul a fost actualizat. - - - Trimitere... - Message shown when interacting with the server - - - Sincronizare... - Message shown when interacting with the server - - - Sincronizare completă. - - - Sincronizarea nu a reușit. - - - Sincronizaţi seiful acum - - - Touch ID - What Apple calls their fingerprint reader. - - - Autentificare în două etape - - - Autentificarea în două etape face ca contul dvs. să fie mai sigur, solicitând să introduceți un cod de securitate dintr-o aplicație de autentificare ori de câte ori vă conectați. Autentificarea în două etape poate fi activată în seiful web bitwarden.com. Doriți să vizitați site-ul acum? - - - Deblocare cu {0} - - - Deblocare cu codul PIN - - - Validare - Message shown when interacting with the server - - - Cod de verificare - - - Vizualizare articol - - - Seif Web Bitwarden - - - Gestionați datele de conectare din orice browser web cu seiful web Bitwarden. - - - Ai pierdut aplicația de autentificare? - - - Articole - Screen title - - - Extensie activată! - - - Pictograme - - - Traduceri - - - Autentificări pentru {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Nu sunt autentificări în seiful dvs. pentru {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Atunci când vedeți o notificare de completare automată Bitwarden, puteți s-o atingeți pentru a lansa serviciul de completare automată. - - - Atingeți această notificare pentru a completa automat o autentificare din seiful dvs. - - - Deschidere setări de accesibilitate - - - 1. În ecranul Accesibilitate din setările Android, atingeți "Bitwarden" sub titlul Servicii. - - - 2. Activați comutatorul și apăsați OK pentru a accepta. - - - Dezactivat - - - Activat - - - Stare - - - Beta - - - Cea mai ușoară modalitate de a adăuga noi intrări în seif este din Serviciul de completare automată Bitwarden. Aflați mai multe despre utilizarea Serviciului de completare automată Bitwarden accesând ecranul "Instrumente". - - - Completare automată - - - Doriți să completați automat sau să vizualizați aceste date de conectare? - - - Sigur doriți să completați automat aceste date de conectare? Nu este o potrivire completă pentru "{0}". - - - Elemente concordante - - - Elemente posibil concordante - - - Căutare - - - Căutați o autentificare automată pentru "{0}". - - - Partajaţi seiful - - - Creați o organizație pentru a partaja în siguranță accesul la datele de conectare cu alţi utilizatori. - - - Scanare atunci când câmpul pentru parole este focalizat - - - Se scanează numai ecranul pentru câmpuri și se oferă o notificare de completare automată ori de câte ori selectați un câmp de parolă. Această setare poate contribui la economisirea duratei de viață a bateriei. - - - Notificare persistentă - - - Oferă întotdeauna o notificare de auto-completare și scanează câmpuri numai după încercarea unei completări automate. Această setare poate contribui la economisirea duratei de viață a bateriei. - - - Scanare permanentă - - - Se scanează întotdeauna ecranul pentru câmpuri și se oferă o notificare automată numai dacă sunt găsite câmpuri de parolă. Aceasta este setarea implicită. - - - Nu se poate deschide aplicaţia "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Aplicaţie de autentificare - For 2FA - - - Introduceţi codul de verificare din 6 cifre din aplicaţia de autentificare. - For 2FA - - - Introduceţi codul de verificare din 6 cifre, care a fost trimis prin e-mail la {0}. - For 2FA - - - Login indisponibil - For 2FA whenever there are no available providers on this device. - - - Acest cont are activată autentificarea în două etape, cu toate acestea, niciun furnizor de autentificare în două etape configurat nu este acceptat pe acest dispozitiv. Utilizaţi un dispozitiv acceptat şi/sau adăugaţi furnizorii suplimentari care sunt mai bine acceptaţi între dispozitive (cum ar fi o aplicaţie de autentificare). - - - Cod de recuperare - For 2FA - - - Memorizare - Remember my two-step login - - - Trimitere e-mail cu codul de verificare din nou - For 2FA - - - Opţiuni autentificare în două etape - - - Utilizaţi o altă metodă de autentificare în două etape - - - Nu s-a putut trimite e-mailul de verificare. Încercați din nou. - For 2FA - - - S-a trimis e-mailul de verificare. - For 2FA - - - Pentru a continua, țineți apăsat YubiKey NEO din partea din spate a dispozitivului sau introduceți YubiKey în portul USB al dispozitivului dvs., apoi atingeți butonul acestuia. - - - Cheie de securitate YubiKey NEO - "YubiKey" is the product name and should not be translated. - - - Adăugaţi un nou ataşament - - - Ataşamente - - - Imposibil de descărcat fișierul. - - - Dispozivul nu poate deschide acest tip de fişiere. - - - Descărcare... - Message shown when downloading a file - - - Acest ataşament are {0}. Sunteţi sigur că doriţi să-l descărcaţi pe dispozitivul dvs? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Cheie autentificare (TOTP) - - - Cod de verificare (TOTP) - Totp code label - - - Cheie de autentificare adăugată. - - - Imposibil de citit cheia de autentificare. - - - Se vă scana automat. - - - Îndreptați camera spre codul QR. - - - Scanare cod QR - - - Cameră - - - Fotografii - - - TOTP copiat! - - - Copiere TOTP - - - Dacă datele dvs. de conectare au o cheie de autentificare atașată, codul de verificare TOTP este copiat automat în clipboard de fiecare dată când efectuați automat completarea datelor de conectare. - - - Dezactivare copiere automată TOTP - - - Un abonament premium este necesar pentru a utiliza această caracteristică. - - - Ataşament adăugat - - - Ataşament şters - - - Alegeți fișierul - - - Fişier - - - Niciun fişier ales - - - Nu există niciun ataşament. - - - Fişierul sursă - - - Caracteristică indisponibilă - - - Mărimea maximă a fişierului este de 100 MB. - - - Veți putea utiliza această caracteristică după ce veți actualiza cheia de criptare. - - - Aflați mai multe - - - URL-ul serverului API - - - Mediu particularizat - - - Pentru utilizatorii avansați. Puteți specifica adresa URL de bază a fiecărui serviciu independent. - - - Adresele URL de mediu au fost salvate. - - - {0} nu este formatat corect. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL-ul serverului de identitate - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Mediu găzduit în mod automat - - - Specificați adresa URL de bază a instalării Bitwarden. - - - URL-ul serverului - - - URL-ul serverului seifului web - - - Atingeți această notificare pentru a vizualiza autentificările din seiful dvs. - - - Câmpuri particularizate - - - Copiere număr - - - Copiere cod CVV2/CVC2 - - - Număr card - - - Cod CVV2/CVC2 - - - Ce tip de articol doriți să adaugați? - - - Tip card - - - Identitate - - - Autentificare - - - Mențiune securizata - - - Adresă 1 - - - Adresă 2 - - - Adresă 3 - - - aprilie - - - august - - - Tip card - - - Deținător card - - - Localitate - - - Companie - - - Țară - - - decembrie - - - Dr. - - - Luna expirării - - - Anul expirării - - - februarie - - - Prenume - - - ianuarie - - - iulie - - - iunie - - - Nume - - - Număr licență - - - martie - - - mai - - - Al doilea prenume - - - Dl - - - Dna - - - Dra - - - noiembrie - - - octombrie - - - Număr CI/pașaport - - - Telefon - - - septembrie - - - CNP - - - Județ - - - Titlu - - - Cod poștal - - - Adresă - - - Expirare - - - Dezactivare pictograme - - - Pictogramele oferă o imagine recognoscibilă alături de fiecare element de conectare din seiful dvs. - - - URL server de pictograme - - - Completare automată cu Bitwarden - - - Seiful este blocat - - - Deschidere seif personal - - - Colecții - - - Nu există elemente în colecție. - - - Nu există elemente în acest dosar. - - - Serviciul de accesibilitate pentru completare automată - - - Serviciul de completare automată Bitwarden folosește Android Autofill Framework pentru a asista completarea informațiilor de autentificare și a celor asociate cardurilor și identităților în alte aplicații de pe dispozitivul dvs. - - - Serviciul de completare automată Bitwarden se folosește pentru a completa informațiile de autentificare și cele asociate cardurilor și identităților în alte aplicații. - - - Setări de completare automată - - - ID facial - What Apple calls their facial recognition reader. - - - Folosire ID facial pentru verificare - - - Folosire ID facial pentru deblocare - - - Verificare ID facial - - - Deblocare cu Windows Hello - - - Verificare cu Windows Hello - - - Windows Hello - - - Nu am reușit să deschidem automat meniul setărilor pentru completare automată. Puteți să navigați manual la meniul setărilor pentru completarea automată din Setările Android > Sistem > Limbi și introducerea textului > Avansate > Serviciu de completare automată. - - - Numele câmpului particularizat - - - Valoare logică - - - Ascuns - - - Text - - - Câmp nou personalizat - - - Ce tip de câmp personalizat doriți să adaugați? - - - Ștergeți - - - URI nou - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Domeniu de bază - - - Implicit - - - Exact - - - Gazdă - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Expresie regulată - A programming term, also known as 'RegEx'. - - - Începe cu - - - Detectare potrivire URI - - - Detectare potrivire - URI match detection for auto-fill. - - - Da și salvează - - - Completează automat și salvează - - - Organizație - An entity of multiple related people (ex. a team or business organization). - - - Țineţi YubiKey-ul dumneavoastră în partea de sus a dispozitivului. - - - Încercaţi din nou - - - Pentru a continua, ţineţi NEO-YubiKey-ul dumneavoastră către partea din spate a dispozitivului. - - - Serviciul de accesibilitate poate fi de ajutor atunci când unele aplicații nu acceptă serviciul standard de completare automată. - - - Parolă actualizată - ex. Date this password was updated - - - Actualizat - ex. Date this item was updated - - - Auto Completare Activată! - - - Trebuie să te loghezi în aplicația Bitwarden înainte să folosești Auto Completarea. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - Recomandăm dezactivarea celorlalte aplicații care folosesc AutoFill din setări dacă nu intenționați să le utilizați. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - Pentru a activa auto completarea pe acest dispozitiv, urmărește aceste instrucțiuni: - - - 1. Accesează Setările iOS - - - 2. Selectează "Parole & Conturi" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Selectează "Bitwarden" - - - Auto Completare Parolă - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.ru.Designer.cs b/src/App/Resources/AppResources.ru.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.ru.resx b/src/App/Resources/AppResources.ru.resx deleted file mode 100644 index 5e7617b25..000000000 --- a/src/App/Resources/AppResources.ru.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - О нас - - - Добавить - Add/create a new entity (verb). - - - Добавить папку - - - Добавить элемент - The title for the add item page. - - - Произошла ошибка. - Alert title when something goes wrong. - - - Назад - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Отмена - Cancel an operation. - - - Копировать - Copy some value to your clipboard. - - - Копировать пароль - The button text that allows a user to copy the login's password to their clipboard. - - - Копировать имя пользователя - The button text that allows a user to copy the login's username to their clipboard. - - - Благодарности - Title for page that we use to give credit to resources that we use. - - - Удалить - Delete an entity (verb). - - - Удаление... - Message shown when interacting with the server - - - Вы действительно хотите удалить? Это действие необратимо. - Confirmation alert message when deleteing something. - - - Изменить - - - Изменить папку - - - Email - Short label for an email address. - - - Адрес email - Full label for a email address. - - - Напишите нам - - - Чтобы получить помощь или оставить отзыв, отправьте нам письмо. - - - Введите PIN-код. - - - Избранные - Title for your favorite items in the vault. - - - Сообщить об ошибке - - - Откройте проблему в нашем репозитории на GitHub. - - - Приложите палец к датчику - - - Папка - Label for a folder. - - - Новая папка создана. - - - Папка удалена. - - - Без папки - Items that have no folder specified go in this special "catch-all" folder. - - - Папки - - - Папка обновлена. - - - Перейти на сайт - The button text that allows user to launch the website to their web browser. - - - Помощь и обратная связь - - - Скрыть - Hide a secret value that is currently shown (password). - - - Пожалуйста, подключитесь к интернету чтобы продолжить. - Description message for the alert when internet connection is required to continue. - - - Требуется подключение к интернету - Title for the alert when internet connection is required to continue. - - - Неверный мастер-пароль. Попробуйте снова. - - - Неверный PIN. Попробуйте снова. - - - Перейти - The button text that allows user to launch the website to their web browser. - - - Войти - The login button text (verb). - - - Вход - Title for login page. (noun) - - - Выйти - The log out button text (verb). - - - Вы действительно хотите выйти? - - - Мастер-пароль - Label for a master password. - - - Больше - Text to define that there are more options things to see. - - - Хранилище - The title for the vault page. - - - Название - Label for an entity name. - - - Нет - - - Заметки - Label for notes. - - - Ok - Acknowledgement. - - - Пароль - Label for a password. - - - Сохранить - Button text for a save operation (verb). - - - Сохранение... - Message shown when interacting with the server - - - Настройки - The title for the settings page. - - - Показать - Reveal a hidden value (password). - - - Элемент был удален. - Confirmation message after successfully deleting a login. - - - Отправить - - - Синхронизация - The title for the sync page. - - - Спасибо - - - Инструменты - The title for the tools page. - - - URI - Label for a uri/url. - - - Использовать отпечаток пальца для разблокировки - - - Имя пользователя - Label for a username. - - - Поле {0} является обязательным. - Validation message for when a form field is left blank and is required to be entered. - - - {0} скопирован. - Confirmation message after suceessfully copying a value to the clipboard. - - - Проверка отпечатка пальца - - - Проверка мастер-пароля - - - Проверка PIN - - - Версия - - - Просмотр - - - Посетите наш сайт - - - Посетите наш веб-сайт, чтобы получить помощь, прочесть новости, связаться с нами и/или узнать больше о том, как использовать Bitwarden. - - - Веб-сайт - Label for a website. - - - Да - - - Аккаунт - - - Ваш аккаунт создан! Теперь вы можете войти в систему. - - - Добавить элемент - - - Расширение - - - Используйте службу специальных возможностей Bitwarden для автоматического заполнения ваших логинов в приложениях и интернете. - - - Служба автозаполнения - - - Избегать неоднозначных символов - - - Расширение Bitwarden - - - Самый простой способ добавить новые логины в ваше хранилище это расширение Bitwarden. Дополнительные сведения об использовании расширения Bitwarden в разделе Инструменты. - - - Используйте Bitwarden в Safari и других приложениях для автозаполнения логинов. - - - Служба автозаполнения Bitwarden - - - Используйте службу специальных возможностей Bitwarden для автоматического заполнения ваших логинов. - - - Изменить email - - - Вы можете изменить свой адрес электронной почты на bitwarden.com. Перейти на сайт сейчас? - - - Изменить мастер-пароль - - - Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас? - - - Закрыть - - - В ближайшее время! - - - Продолжить - - - Скопировано! - - - Пароль скопирован! - - - Имя пользователя скопировано! - - - Создать аккаунт - - - Создание аккаунта... - Message shown when interacting with the server - - - Изменение элемента - - - Включить автоматическую синхронизацию - - - Введите email учетной записи для получения подсказки к мастер-паролю. - - - Повторно включить расширение - - - Почти готово! - - - Включить расширение - - - В Safari, найдите Bitwarden, используя значок Поделиться (прокрутите вправо в нижней строке меню). - Safari is the name of apple's web browser - - - Получите мгновенный доступ к вашим паролям! - - - Вы готовы войти! - - - См. поддерживаемые приложения - - - Теперь ваши логины легко доступны из Safari, Chrome и других поддерживаемых приложений. - - - В Safari и Chrome, найдите Bitwarden, используя значок Поделиться (прокрутите вправо в нижней строке меню). - - - Нажмите значок Bitwarden в меню, чтобы запустить расширение. - - - Чтобы включить Bitwarden в Safari и других приложениях, нажмите Еще в нижней строке меню. - - - Избранный - - - отпечатком - - - Сгенерировать пароль - - - Получить подсказку к мастер-паролю - - - Импорт элементов - - - Вы можете импортировать свои элементы в свое веб-хранилище на bitwarden.com. Перейти на сайт сейчас? - - - Быстрый импорт ваших элементов из других менеджеров паролей. - - - Последняя синхронизация: - - - Длина - - - Заблокировать - - - Через 15 минут - - - Через 1 час - - - Через 1 минуту - - - Через 4 часа - - - Немедленно - - - Параметры блокировки - - - Вход... - Message shown when interacting with the server - - - Войдите или создайте новый аккаунт для доступа к вашему безопасному хранилищу. - - - Управление - - - Неверное подтверждение пароля. - - - Мастер-пароль - ключ к вашему безопасному хранилищу. Он очень важен, поэтому не забывайте его. Восстановить мастер-пароль невозможно. - - - Подсказка к мастер-паролю (необяз.) - - - Подсказка к мастер-паролю может помочь вам его вспомнить. - - - Мастер-пароль должен содержать не менее 8 символов. - - - Мин. кол-во цифр - Minimum numeric characters for password generator settings - - - Мин. кол-во символов - Minimum special characters for password generator settings - - - Дополнительные настройки - - - Перед использованием расширения необходимо войти в основное приложение Bitwarden. - - - Никогда - - - Новый элемент создан. - - - В вашем хранилище нет избранного. - - - В вашем хранилище нет элементов. - - - В вашем хранилище нет элементов для этого сайта/приложения. Нажмите чтобы добавить. - - - У логина не заданы имя пользователя и пароль. - - - Ok, понятно! - Confirmation, like "Ok, I understand it" - - - Параметры по умолчанию задаются из генератора паролей основного приложения Bitwarden. - - - Опции - - - Прочее - - - Пароль сгенерирован. - - - Генератор паролей - - - Подсказка к паролю - - - Мы отправили вам email с подсказкой к мастер-паролю. - - - Вы хотите перезаписать текущий пароль? - - - Bitwarden обеспечивает автоматическую синхронизацию хранилища при помощи push-уведомлений. Для вашего максимального удобства, пожалуйста, выберите "Разрешить" при появлении предложения включить push-уведомления. - Push notifications for apple products - - - Оцените приложение - - - Пожалуйста, подумайте о том, чтобы помочь нам хорошим отзывом! - - - Рейтинги App Store обновляются с каждой новой версией Bitwarden. Пожалуйста, подумайте о том, чтобы помочь нам хорошим отзывом! - - - Создать новый пароль - - - Введите мастер-пароль повторно - - - Поиск в хранилище - - - Безопасность - - - Прогресс разработки - - - Выбрать - - - Установка PIN - - - Введите 4-значный PIN-код для разблокировки приложения. - - - Информация об элементе - - - Элемент обновлен. - - - Отправка... - Message shown when interacting with the server - - - Синхронизация... - Message shown when interacting with the server - - - Синхронизация завершена. - - - Сбой синхронизации. - - - Синхронизировать сейчас - - - Touch ID - What Apple calls their fingerprint reader. - - - Двухфакторная аутентификация - - - Двухфакторная аутентификация делает ваш аккаунт более защищенным, требуя подтверждения входа на другом устройстве, например, ключом безопасности, приложением-аутентификатором, SMS, телефонным звонком или email. Двухфакторная аутентификация включается на bitwarden.com. Перейти на сайт сейчас? - - - Разблокировать {0} - - - Разблокировать PIN-кодом - - - Проверка - Message shown when interacting with the server - - - Код подтверждения - - - Просмотр элемента - - - Веб-хранилище Bitwarden - - - Управляйте своими элементами из любого браузера при помощи веб-хранилища Bitwarden. - - - Потеряли доступ к приложению для аутентификации? - - - Элементы - Screen title - - - Расширение активировано! - - - Значки - - - Локализации - - - Элементы для {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - В вашем хранилище нет элементов для {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Увидев уведомление Bitwarden нажмите на него, чтобы запустить службу автозаполнения. - - - Коснитесь этого уведомления для автоматического заполнения. - - - Открыть настройки специальных возможностей - - - 1. На экране настроек специальных возможностей Android под заголовком Сервисы коснитесь Bitwarden. - - - 2. Активируйте переключатель и нажмите OK для подтверждения. - - - Отключено - - - Включено - - - Статус - - - Beta - - - Самый простой способ добавить новые логины в ваше хранилище это служба автозаполнения Bitwarden. Дополнительные сведения об использовании службы автозаполнения Bitwarden в разделе Инструменты. - - - Автозаполнение - - - Выполнить автоматическое заполнение или просмотреть этот элемент? - - - Вы действительно хотите использовать этот элемент? Это неточное соответствие для "{0}". - - - Соответствующие элементы - - - Возможные элементы - - - Поиск - - - Вы ищете элемент для "{0}". - - - Поделиться вашим хранилищем - - - Создайте организацию, чтобы безопасно делиться своими элементами с другими пользователями. - - - Сканировать выбор поля пароля - - - Сканировать и выводить уведомление автозаполнения только при выборе поля ввода пароля. Этот параметр менее энергозатратен. - - - Постоянное уведомление - - - Сканирование выполняется только после нажатия постоянно отображаемого уведомления. Этот параметр менее энергозатратен. - - - Сканировать всегда - - - Сканирование выполняется постоянно. Уведомление об автозаполнении появляется при обнаружении поля пароля. Это параметр по умолчанию. - - - Не удается открыть приложение "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Приложение-аутентификатор - For 2FA - - - Введите 6-значный код подтверждения из вашего приложения-аутентификатора. - For 2FA - - - Введите 6-значный код подтверждения, который был отправлен на {0}. - For 2FA - - - Вход недоступен - For 2FA whenever there are no available providers on this device. - - - Для этой учетной записи включена двухфакторная аутентификация, однако ни один из настроенных вариантов аутентификации не поддерживается на этом устройстве. Используйте поддерживаемое устройство и/или добавьте другие варианты, которые поддерживаются на большинстве устройств (например, приложение-аутентификатор). - - - Код восстановления - For 2FA - - - Запомнить меня - Remember my two-step login - - - Отправить код подтверждения еще раз - For 2FA - - - Параметры двухфакторной аутентификации - - - Использовать другой метод двухфакторной аутентификации - - - Не удалось отправить письмо подтверждения. Повторить. - For 2FA - - - Отправлено письмо подтверждения. - For 2FA - - - Для продолжения приложите ваш YubiKey NEO к задней панели устройства или вставьте его в USB-порт вашего устройства и нажмите его кнопку. - - - Ключ безопасности YubiKey - "YubiKey" is the product name and should not be translated. - - - Добавить новое вложение - - - Вложения - - - Не удается загрузить файл. - - - Устройство не может открыть этот тип файла. - - - Загрузка... - Message shown when downloading a file - - - Это вложение имеет размер {0}. Вы действительно хотите загрузить его на устройство? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Ключ проверки подлинности (TOTP) - - - Код подтверждения (TOTP) - Totp code label - - - Ключ проверки подлинности добавлен. - - - Не удается прочитать ключ проверки подлинности. - - - Сканирование будет происходить автоматически. - - - Наведите камеру на QR-код. - - - Сканировать QR-код - - - Камера - - - Фотографии - - - TOTP скопирован! - - - Копировать TOTP - - - Если к вашему логину прикреплен ключ аутентификации, то код подтверждения TOTP будет скопирован при автозаполнении логина. - - - Отключить копирование TOTP - - - Для использования этой функции требуется Премиум. - - - Вложение добавлено - - - Вложение удалено - - - Выбрать файл - - - Файл - - - Файл не выбран - - - Нет вложений. - - - Источник файла - - - Функция недоступна - - - Максимальный размер файла 100 МБ. - - - Вы не можете использовать эту функцию, пока не обновите свой ключ шифрования. - - - Узнайть больше - - - API URL-адреса сервера - - - Настройка среды - - - Для продвинутых пользователей. Можно указать URL-адреса отдельно для каждой службы. - - - URL-адреса среды сохранены. - - - {0} это неправильный формат. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL-адрес сервера идентификации - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Среда собственного хостинга - - - Укажите URL-адрес Bitwarden на вашем сервере. - - - URL-адрес сервера - - - URL-адрес сервера веб-хранилища - - - Коснитесь этого уведомления, чтобы посмотреть элементы из вашего хранилища. - - - Пользовательские поля - - - Копировать номер - - - Копировать код безопасности - - - Номер - - - Код безопасности - - - Какой тип элемента вы хотите добавить? - - - Карта - - - Личность - - - Логин - - - Защищенная заметка - - - Строка адреса 1 - - - Строка адреса 2 - - - Строка адреса 3 - - - Апрель - - - Август - - - Тип карты - - - Имя владельца карты - - - Город / Поселок - - - Компания - - - Страна - - - Декабрь - - - Тов. - - - Месяц - - - Год - - - Февраль - - - Имя - - - Январь - - - Июль - - - Июнь - - - Фамилия - - - ИНН - - - Март - - - Май - - - Отчество - - - Г-н - - - Г-жа - - - Проф. - - - Ноябрь - - - Октябрь - - - Номер паспорта - - - Телефон - - - Сентябрь - - - Номер социального страхования - - - Регион / Область - - - Обращение - - - Почтовый индекс - - - Адрес - - - Срок действия - - - Отключить значки веб-сайтов - - - Значки веб-сайтов отображаются рядом с каждым элементом в вашем хранилище. - - - URL-адрес сервера значков - - - Автозаполнение Bitwarden - - - Хранилище заблокировано - - - Перейти в мое хранилище - - - Коллекции - - - В этой коллекции нет элементов. - - - В этой папке нет элементов. - - - Служба специальных возможностей автозаполнения - - - Служба автозаполнения Bitwarden использует Android Autofill Framework для заполнения логинов, номеров карт и личной информации в других приложениях на устройстве. - - - Используйте службу автозаполнения Bitwarden для заполнения логинов, номеров карт и личной информации в других приложениях. - - - Открыть настройки автозаполнения - - - Face ID - What Apple calls their facial recognition reader. - - - Использовать Face ID для верификации. - - - Использовать Face ID для разблокировки - - - Верификация Face ID - - - Разблокировать с Windows Hello - - - Верифицировать с Windows Hello - - - Windows Hello - - - Не удалось автоматически открыть меню настроек автозаполнения Android. Вы можете перейти в меню настроек автозаполнения вручную из настроек Android > Система > Языки и ввод > Дополнительно > Служба автозаполнения. - - - Название пользовательского поля - - - Логическое - - - Скрытое - - - Текстовое - - - Новое пользовательское поле - - - Какой тип пользовательского поля вы хотите добавить? - - - Удалить - - - Новый URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Основной домен - - - По умолчанию - - - Точно - - - Хост - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Регулярное выражение - A programming term, also known as 'RegEx'. - - - Начинается с - - - Обнаружение совпадений URI - - - Обнаружение совпадений - URI match detection for auto-fill. - - - Да, и сохранить - - - Заполнить и сохранить - - - Организация - An entity of multiple related people (ex. a team or business organization). - - - Удерживайте Yubikey в верхней части устройства. - - - Еще раз - - - Для продолжения, удерживайте YubiKey NEO у задней панели устройства. - - - Служба специальных возможностей может быть полезна в случае, когда приложения не поддерживают стандартную службу автозаполнения. - - - Пароль обновлен - ex. Date this password was updated - - - Обновлено - ex. Date this item was updated - - - Автозаполнение активировано! - - - Перед использованием автозаполнения необходимо войти в основное приложение Bitwarden. - - - Авторизация никогда не была проще - теперь ваши логины доступны прямо с клавиатуры. - - - Рекомендуется отключить все другие приложения автозаполнения в настройках, если вы не планируете их использовать. - - - Получите доступ к вашему хранилищу прямо с клавиатуры для максимально быстрого автозаполнения. - - - Чтобы включить автозаполнение паролей на устройстве, выполните следующие действия: - - - 1. Перейдите в 'Настройки' iOS - - - 2. Нажмите 'Пароли и учетные записи' - - - 3. Нажмите 'Автозаполнение паролей' - - - 4. Включите автозаполнение - - - 5. Выберите Bitwarden - - - Автозаполнение паролей - - - Самый простой способ добавить новые логины в хранилище - использовать расширение автозаполнения паролей Bitwarden. Дополнительные сведения об использовании расширения автозаполнения паролей Bitwarden в разделе Инструменты. - - diff --git a/src/App/Resources/AppResources.sk.Designer.cs b/src/App/Resources/AppResources.sk.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.sk.resx b/src/App/Resources/AppResources.sk.resx deleted file mode 100644 index 54546baea..000000000 --- a/src/App/Resources/AppResources.sk.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - O aplikácii - - - Pridať - Add/create a new entity (verb). - - - Pridať priečinok - - - Pridať položku - The title for the add item page. - - - Vyskytla sa chyba. - Alert title when something goes wrong. - - - Späť - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - Zrušiť - Cancel an operation. - - - Kopírovať - Copy some value to your clipboard. - - - Kopírovať heslo - The button text that allows a user to copy the login's password to their clipboard. - - - Kopírovať používateľské meno - The button text that allows a user to copy the login's username to their clipboard. - - - Poďakovania - Title for page that we use to give credit to resources that we use. - - - Odstrániť - Delete an entity (verb). - - - Odstraňuje sa... - Message shown when interacting with the server - - - Naozaj chcete odstrániť? Nedá sa to vrátiť späť. - Confirmation alert message when deleteing something. - - - Upraviť - - - Upraviť priečinok - - - Email - Short label for an email address. - - - Emailová adresa - Full label for a email address. - - - Napíšte nám - - - Získajte pomoc alebo zanechajte spätnú väzbu zaslaním emailu. - - - Zadajte PIN kód. - - - Obľúbené - Title for your favorite items in the vault. - - - Nahlásiť chybu - - - Nahlásiť problém v našom GitHub repozitári. - - - Použite odtlačok prsta na overenie. - - - Priečinok - Label for a folder. - - - Nový priečinok vytvorený. - - - Priečinok bol odstránený. - - - Žiadny priečinok - Items that have no folder specified go in this special "catch-all" folder. - - - Priečinky - - - Priečinok aktualizovaný. - - - Prejsť na stránku - The button text that allows user to launch the website to their web browser. - - - Pomoc a spätná väzba - - - Skryť - Hide a secret value that is currently shown (password). - - - Pripojte sa k internetu pred pokračovaním. - Description message for the alert when internet connection is required to continue. - - - Vyžaduje sa internetové pripojenie - Title for the alert when internet connection is required to continue. - - - Neplatné hlavné heslo. Skúste znova. - - - Neplatný PIN kód. Skúste znova. - - - Spustiť - The button text that allows user to launch the website to their web browser. - - - Prihlásiť sa - The login button text (verb). - - - Prihlásenie - Title for login page. (noun) - - - Odhlásiť sa - The log out button text (verb). - - - Naozaj sa chcete odhlásiť? - - - Hlavné heslo - Label for a master password. - - - Viac - Text to define that there are more options things to see. - - - Môj trezor - The title for the vault page. - - - Meno - Label for an entity name. - - - Nie - - - Poznámky - Label for notes. - - - Ok - Acknowledgement. - - - Heslo - Label for a password. - - - Uložiť - Button text for a save operation (verb). - - - Ukladá sa... - Message shown when interacting with the server - - - Nastavenia - The title for the settings page. - - - Zobraziť - Reveal a hidden value (password). - - - Položka bola odstránená. - Confirmation message after successfully deleting a login. - - - Potvrdiť - - - Synchronizácia - The title for the sync page. - - - Ďakujeme Vám - - - Nástroje - The title for the tools page. - - - URI - Label for a uri/url. - - - Použiť odtlačkov prsta na odomknutie - - - Používateľské meno - Label for a username. - - - {0} je povinné. - Validation message for when a form field is left blank and is required to be entered. - - - {0} bolo skopírované. - Confirmation message after suceessfully copying a value to the clipboard. - - - Overenie odtlačku prsta - - - Overenie hlavného hesla - - - Overiť PIN - - - Verzia - - - Zobraziť - - - Navštívte našu stránku - - - Navštívte našu stránku pre získanie pomoci, noviniek, poslať nám email, alebo sa dozvedieť viac, ako používať bitwarden. - - - Webstránka - Label for a website. - - - Áno - - - Účet - - - Váš nový účet bol vytvorený! Teraz sa môžete prihlásiť. - - - Pridať položku - - - Rozšírenie aplikácie - - - Použite službu zjednodušenia prístupu bitwarden pre automatické vypĺňanie v aplikáciách a na webe. - - - Služba automatického vypĺňania - - - Vyhnúť sa dvojvýznamovým znakom - - - bitwarden rozšírenie aplikácie - - - Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z bitwarden rozšírenia aplikácie. Ďalšie informácie o používaní bitwarden rozšírenia aplikácie získate prechodom do obrazovky Nástrojov. - - - Použiť bitwarden v Safari a iných aplikáciach pre automatické vypĺňanie prihlasovacích údajov. - - - Služba automatického vypĺňania bitwarden - - - Použite službu zjednodušenia prístupu bitwarden pre automatické vypĺňanie prihlasovacích údajov. - - - Zmeniť email - - - Svoju emailovú adresu môžete zmeniť na webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz? - - - Zmeniť hlavné heslo - - - Svoje hlavné heslo môžete zmeniť na webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz? - - - Zavrieť - - - Už čoskoro! - - - Pokračovať - - - Skopírované! - - - Heslo skopírované! - - - Používateľské meno skopírované! - - - Vytvoriť účet - - - Vytvára sa účet... - Message shown when interacting with the server - - - Upraviť položku - - - Zapnúť automatickú synchronizáciu - - - Zadajte emailovú adresu na zaslanie nápovedy pre vaše hlavné heslo. - - - Znovu zapnúť rozšírenie aplikácie - - - Takmer hotovo! - - - Zapnúť rozšírenie aplikácie - - - V Safari nájdete bitwarden použitím ikony zdieľania (tip: posúvajte doprava v spodnom riadku menu). - Safari is the name of apple's web browser - - - Získajte okamžitý prístup k vašim heslám! - - - Ste pripravený prihlásiť sa! - - - Zobraziť podporované aplikácie - - - Vaše prihlasovacie údaje sú teraz ľahko prístupné zo Safari, Chrome a ďalších podporovaných aplikácií. - - - V Safari a Chrome nájdete bitwarden použitím ikony zdieľania (tip: posúvajte doprava v spodnom riadku menu). - - - Klepnite na ikonu bitwarden v menu na spustenie rozšírenia. - - - Pre zapnutie bitwardenu v Safari a iných aplikáciach, klepnite na ikonu "viac" v spodnom riadku menu. - - - Obľúbené - - - Odtlačok prsta - - - Generovať heslo - - - Získať nápovedu pre hlavné heslo - - - Importovať položky - - - Môžete hromadne importovať položky z webového trezoru bitwarden.com. Chcete navštíviť túto stránku teraz? - - - Rýchlo hromadne importovať vaše prihlasovacie údaje z iných aplikácií na správu hesiel. - - - Posledná synchronizácia: - - - Dĺžka - - - Zámok - - - 15 minút - - - 1 hodina - - - 1 minúta - - - 4 hodiny - - - Okamžite - - - Možnosti zámku - - - Prihlasovanie... - Message shown when interacting with the server - - - Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru. - - - Spravovať - - - Potvrdenie hesla nie je správne. - - - Hlavné heslo je heslo, ktoré použijete na prístup k svojmu trezoru. Je veľmi dôležité, aby ste svoje hlavné heslo nezabudli. Neexistuje možnosť, ako heslo obnoviť v prípade, že ho zabudnete. - - - Nápoveda k hlavnému heslo (voliteľné) - - - Nápoveda k hlavnému heslu vám môže pomôcť spomenúť si na heslo, ak ho zabudnete. - - - Hlavné heslo musí obsahovať aspoň 8 znakov. - - - Minimum číslic - Minimum numeric characters for password generator settings - - - Minimum špeciálnych - Minimum special characters for password generator settings - - - Ďalšie nastavenia - - - Pred použitím rozšírenia sa najskôr musíte prihlásiť v hlavnej bitwarden aplikácií. - - - Nikdy - - - Nová položka vytvorená. - - - Vo vašom trezore nie sú žiadné obľúbené. - - - V trezore sa nenachádzajú žiadne položky. - - - Pre túto stránku v trezore neexistujú žiadne prihlasovacie údaje. Kliknite pre pridanie. - - - Toto prihlásenie nemá nakonfigurované používateľské meno alebo heslo. - - - Ok, rozumiem! - Confirmation, like "Ok, I understand it" - - - Predvolené možnosti sa nastavujú v nástroji na generovanie hesla v hlavnej bitwarden aplikácií. - - - Možnosti - - - Ostatné - - - Heslo vytvorené. - - - Generátor hesla - - - Nápoveda pre heslo - - - Poslali sme vám email s nápovedou k hlavnému heslu. - - - Naozaj chcete prepísať aktuálne heslo? - - - bitwarden udržuje váš trezor automaticky synchronizovaný použitím push notifikácií. Pre čo najlepší priebeh, prosím potvrďte nasledujúcu výzvu pre zapnutie push notifikácií. - Push notifications for apple products - - - Ohodnotiť aplikáciu - - - Prosíme, zvážte pomôcť nám dobrým hodnotením! - - - Hodnotenia v App Store sú resetované pri každej novej verzii bitwarden. Prosíme, zvážte pomôcť nám dobrým hodnotením! - - - Vygenerovať nové heslo - - - Znovu zadajte hlavné heslo - - - Prehľadávať trezor - - - Zabezpečenie - - - Pozrieť postup vývoja - - - Vybrať - - - Nastaviť PIN - - - Zadajte 4-ciferný PIN kód na odomknutie aplikácie. - - - Informácie o položke - - - Položka bola aktualizovaná. - - - Odosielam... - Message shown when interacting with the server - - - Synchronizujem... - Message shown when interacting with the server - - - Synchronizácia kompletná. - - - Synchronizácia zlyhala. - - - Synchronizovať trezor teraz - - - Touch ID - What Apple calls their fingerprint reader. - - - Dvojstupňové prihlásenie - - - Dvojstupňové prihlasovanie robí váš účet bezpečnejsí vyžadovaním bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť na webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz? - - - Odomknúť s {0} - - - Odomknúť s PIN kódom - - - Overujem - Message shown when interacting with the server - - - Overovací kód - - - Zobraziť položku - - - bitwarden webový trezor - - - Spravujte vaše prihlasovacie údaje z akéhokoľvek prehliadača s webovým trezorom bitwarden. - - - Stratili ste overovaciu aplikáciu? - - - Položky - Screen title - - - Rozšírenie aktivované! - - - Ikony - - - Preklady - - - Položky pre {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - V trezore sa nenachádzajú žiadne položky pre {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Keď uvidíte notifikáciu automatického vypĺňania bitwarden, môžete na ňu klepnúť pre spustenie služby automatického vypĺňania. - - - Ťuknite na túto notifikáciu pre automatické vyplnenie prihlasovacích údajov z vášho trezora. - - - Otvoriť nastavenia zjednodušenia prístupu - - - 1. Otvorte obrazovku Uľahčenia prístupu, klepnite na "bitwarden" pod nadpisom Služby. - - - 2. Zapnite a stlačte OK pre potvrdenie. - - - Vypnuté - - - Zapnuté - - - Stav - - - Beta - - - Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z bitwarden služby automatického vypĺňania. Ďalšie informácie o používaní bitwarden automatického vypĺňania získate prechodom do obrazovky Nástrojov. - - - Automatické dopĺňanie - - - Chcete automaticky vyplniť alebo zobraziť toto prihlásenie? - - - Naozaj chcete automaticky vyplniť toto prihlásenie? Nie je to úplná zhoda pre {0}. - - - Zodpovedajúce položky - - - Možné zodpovedajúce položky - - - Hladať - - - Hľadáte automaticky vypľňané prihlásenie pre "{0}". - - - Zdieľať trezor - - - Vytvorte organizáciu pre bezpečné zdieľanie vašich prihlasovacích údajov s inými používateľmi. - - - Skenovať ak je pole pre heslo označené - - - Skenovať polia na obrazovke a ponúknuť automatické vypĺňanie len ak je pole na heslo označené. Toto nastavenie môže ušetriť batériu. - - - Trvalá notifikácia - - - Vždy ponúknuť automatické vypĺňanie a skenovať polia až po pokuse o vyplnenie. Toto nastavenie môže ušetriť batériu. - - - Vždy skenovať - - - Vždy skenovať obrazovku pre polia a ponúknuť notifikáciu na automatické vyplnenie ak sú nájdené polia na heslo. Toto je predvolené nastavenie. - - - Nemožno spustiť aplikáciu "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Overovacia aplikácia - For 2FA - - - Zadajte 6-miestny verifikačný kód z vašej overovacej aplikácie. - For 2FA - - - Zadajte 6-miestny verifikačný kód, ktorý bol zaslaný emailom na {0}. - For 2FA - - - Prihlásenie nedostupné - For 2FA whenever there are no available providers on this device. - - - Tento účet má povolené dvojstupňové prihlásenie, ale žiadny z konfigurovaných poskytovateľov nie je podporovaný na tomto zariadení. Prosím, použite podporované zariadenie a/alebo pridajte poskytovateľa, ktorý je lepšie podporovaný na zariadeniach (napríklad overovaciu aplikáciu). - - - Záchranný kód - For 2FA - - - Zapamätať si ma - Remember my two-step login - - - Znovu zaslať overovací kód emailom - For 2FA - - - Možnosti dvojstupňového prihlásenia - - - Použiť inú dvojstupňovú metódu prihlásenia - - - Nepodarilo sa odoslať overovací email. Skúste znova. - For 2FA - - - Overovací email bol odoslaný. - For 2FA - - - Pre pokračovanie podržte vaše YubiKey NEO na zadnej strane zariadenia. - - - YubiKey NEO bezpečnostný kľúč - "YubiKey" is the product name and should not be translated. - - - Pridať novú prílohu - - - Prílohy - - - Nepodarilo sa stiahnuť súbor. - - - Vaše zariadenie nevie otvoriť tento typ súboru. - - - Sťahovanie... - Message shown when downloading a file - - - Táto príloha je veľká {0}. Naozaj ju chcete stiahnúť do vášho zariadenia? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Kľúč overovateľa (TOTP) - - - Overovací kód (TOTP) - Totp code label - - - Kľúč overovateľa pridaný. - - - Overovací kľúč sa nedá čítať. - - - Skenovanie prebehne automaticky. - - - Namierte fotoaparát na QR kód. - - - Skenovať QR kód - - - Kamera - - - Fotky - - - TOTP skopírované! - - - Kopírovať TOTP - - - Ak je kľúč overovateľa spojený s vašim prihlásením, TOTP verifikačný kód bude automaticky skopírovaný do schránky vždy, keď použijete automatické vypĺňanie. - - - Zakázať automatické kopírovanie TOTP - - - Pre použitie tejto funkcie je potrebné prémiové členstvo. - - - Príloha pridaná - - - Príloha odstránená - - - Vybrať súbor - - - Súbor - - - Nebol vybratý žiadny súbor - - - Nie sú žiadne prílohy. - - - Zdroj súboru - - - Funkcia nie je k dispozícii - - - Maximálna veľkosť súboru je 100 MB. - - - Túto funkciu nemožno použiť, kým neaktualizujete svoj šifrovací kľúč. - - - Zistiť viac - - - URL API servera - - - Vlastné prostredie - - - Pre pokročilých používateľov. Môžete špecifikovať základnú URL pre každú službu nezávisle. - - - URL prostredia boli uložené. - - - {0} nie je v správnom formáte. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL servera identít - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Sebou hosťované prostredie - - - Špecifikujte základnú URL lokálne hosťovanej inštalácie bitwarden. - - - URL servera - - - URL servera webového trezora - - - Kliknite na toto upozornenie pre zobrazenie položiek v trezore. - - - Vlastné polia - - - Kopírovať číslo - - - Kopírovať bezpečnostný kód - - - Číslo - - - Bezpečnostný kód - - - Aký typ položky chcete pridať? - - - Karta - - - Identita - - - Prihlásenie - - - Zabezpečená poznámka - - - Adresa 1 - - - Adresa 2 - - - Adresa 3 - - - Apríl - - - August - - - Značka - - - Meno vlastníka karty - - - Mesto - - - Spoločnosť - - - Krajina - - - December - - - Dr - - - Mesiac expirácie - - - Rok expirácie - - - Február - - - Krstné meno - - - Január - - - Júl - - - Jún - - - Priezvisko - - - Číslo vodičského preukazu - - - Marec - - - Máj - - - Druhé meno - - - Pán - - - Pani - - - Slečna - - - November - - - Október - - - Číslo pasu - - - Telefón - - - September - - - Číslo poistenca sociálnej poisťovne - - - Región - - - Oslovenie - - - PSČ - - - Adresa - - - Expirácia - - - Zakázať ikony webových stránok - - - Ikony stránok poskytujú rozoznateľný obrázok vedľa každého prihlasovacieho údaju vo webovom trezore. - - - URL servera ikôn - - - Automaticky vyplniť s bitwarden - - - Trezor je uzamknutý - - - Ísť do trezoru - - - Zbierky - - - V zbierke sa nenachádzajú žiadne položky. - - - V tomto priečinku sa nenachádzajú žiadne položky. - - - Služba automatického vypĺňania pre zjednodušený prístup - - - Bitwarden služba automatického vypĺňania využíva Android Autofill Framework pre zjednodušenie vypĺňania prihlasovacích údajov, kreditných kariet a identít v aplikáciách vo vašom zariadení. - - - Použite službu zjednodušenia prístupu bitwarden pre automatické vypĺňanie. - - - Otvoriť nastavenia automatického vypĺňania - - - Face ID - What Apple calls their facial recognition reader. - - - Overiť pomocou Face ID. - - - Odomknúť pomocou Face ID - - - Overiť Face ID - - - Odomknúť pomocou Windows Hello - - - Overiť pomocou Windows Hello - - - Windows Hello - - - Nepodarilo sa automaticky otvoriť nastavenia automatického dopĺňania pre systém Android. Môžete otvoriť nastavenie manuálne cez Nastavenia > Systém > Jazyky a vstup > Rozšírené > Služba automatického dopĺňania. - - - Názov vlastného poľa - - - Áno/Nie - - - Skryté - - - Text - - - Nové vlastné pole - - - Aký typ poľa chcete pridať? - - - Odstrániť - - - Nové URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Základná doména - - - Predvolené - - - Presný - - - Hostiteľ - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Regulárny výraz - A programming term, also known as 'RegEx'. - - - Začína na - - - Spôsob mapovania URI - - - Spôsob mapovania - URI match detection for auto-fill. - - - Áno, a uložiť - - - Auto-vyplniť a uložiť - - - Organizácia - An entity of multiple related people (ex. a team or business organization). - - - Držte vaše Yubikey blízko hornej časti prístroja. - - - Skúsiť Znovu - - - Ak chcete pokračovať, podržte YubiKey NEO na zadnej strane prístroja. - - - Služba dostupnosti môže byť užitočné pre aplikácie, ktoré nepodporujú štandardnú službu automatického dopĺňania. - - - Heslo bolo aktualizované - ex. Date this password was updated - - - Aktualizované - ex. Date this item was updated - - - Automatické dopĺňanie aktivované! - - - Pred použitím automatického vypĺňania sa najskôr musíte prihlásiť v hlavnej bitwarden aplikácií. - - - Pri prihlasovaní do aplikácií a westránok sú teraz vaše prihlasovacie údaje dostupné priamo z klávesnice. - - - Odporúčame vám vypnúť ostatné aplikácie pre automatické dopĺňanie, ak ich neplánujete používať. - - - Pri zadávaní hesiel pristupujte v vášmu trezoru priamo z klávesnice. - - - Ak chcete povoliť automatické vypĺňanie, postupujte podľa nasledujúcich inštrukcií: - - - 1. Prejdite do aplikácie "Nastavenia" pre iOS - - - 2. Ťuknite na položku "Heslá & účty" - - - 3. Klepnite na možnosť "Automaticky vypĺňať heslá" - - - 4. Zapnite automatické dopĺňanie - - - 5. Vyberte "Bitwarden" - - - Automatické vypĺňanie hesiel - - - Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z bitwarden rozšírenia pre automatické vypĺňanie. Ďalšie informácie o používaní rozšírenia pre automatické dopĺňanie získate prechodom na obrazovku Nástrojov. - - diff --git a/src/App/Resources/AppResources.sv.Designer.cs b/src/App/Resources/AppResources.sv.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.sv.resx b/src/App/Resources/AppResources.sv.resx deleted file mode 100644 index 68580a4d0..000000000 --- a/src/App/Resources/AppResources.sv.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Om - - - Skapa - Add/create a new entity (verb). - - - Skapa mapp - - - Lägg till objekt - The title for the add item page. - - - Ett fel har inträffat. - Alert title when something goes wrong. - - - Tillbaka - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Avbryt - Cancel an operation. - - - Kopiera - Copy some value to your clipboard. - - - Kopiera lösenord - The button text that allows a user to copy the login's password to their clipboard. - - - Kopiera användarnamn - The button text that allows a user to copy the login's username to their clipboard. - - - Credits - Title for page that we use to give credit to resources that we use. - - - Ta bort - Delete an entity (verb). - - - Tar bort... - Message shown when interacting with the server - - - Är du säker på att du vill ta bort? Det här går inte att ångra. - Confirmation alert message when deleteing something. - - - Ändra - - - Ändra mapp - - - E-post - Short label for an email address. - - - E-postadress - Full label for a email address. - - - Mejla oss - - - Mejla oss direkt för att få hjälp eller för att lämna återkoppling. - - - Ange din PIN-kod. - - - Favoriter - Title for your favorite items in the vault. - - - Skicka en buggrapport - - - Öppna ett problemärende på vårt GitHub-arkiv. - - - Använd ditt fingeravtryck för att verifiera. - - - Mapp - Label for a folder. - - - Ny mapp skapad. - - - Mapp borttagen. - - - Ingen mapp - Items that have no folder specified go in this special "catch-all" folder. - - - Mappar - - - Mapp uppdaterad. - - - Gå till webbplats - The button text that allows user to launch the website to their web browser. - - - Hjälp och återkoppling - - - Göm - Hide a secret value that is currently shown (password). - - - Var god anslut till internet innan du fortsätter. - Description message for the alert when internet connection is required to continue. - - - Internetanslutning krävs - Title for the alert when internet connection is required to continue. - - - Ogiltigt huvudlösenord. Försök igen. - - - Ogiltig PIN-kod. Försök igen. - - - Öppna - The button text that allows user to launch the website to their web browser. - - - Logga in - The login button text (verb). - - - Inloggning - Title for login page. (noun) - - - Logga ut - The log out button text (verb). - - - Är du säker på att du vill logga ut? - - - Huvudlösenord - Label for a master password. - - - Mer - Text to define that there are more options things to see. - - - Mitt valv - The title for the vault page. - - - Namn - Label for an entity name. - - - Nej - - - Anteckningar - Label for notes. - - - OK - Acknowledgement. - - - Lösenord - Label for a password. - - - Spara - Button text for a save operation (verb). - - - Sparar... - Message shown when interacting with the server - - - Inställningar - The title for the settings page. - - - Visa - Reveal a hidden value (password). - - - Tog bort objekt. - Confirmation message after successfully deleting a login. - - - Skicka - - - Synka - The title for the sync page. - - - Tack - - - Verktyg - The title for the tools page. - - - URI (länk) - Label for a uri/url. - - - Använd fingeravtryck för att låsa upp - - - Användarnamn - Label for a username. - - - {0}-fältet krävs. - Validation message for when a form field is left blank and is required to be entered. - - - {0} har kopierats. - Confirmation message after suceessfully copying a value to the clipboard. - - - Verifiera fingeravtryck - - - Verifiera huvudlösenord - - - Verifiera PIN-kod - - - Version - - - Visa - - - Besök vår webbplats - - - Besök vår webbplats för att få hjälp, nyheter, mejla oss och/eller lära er mer om hur man använder Bitwarden. - - - Webbsida - Label for a website. - - - Ja - - - Konto - - - Ditt nya konto har blivit skapat! Du kan nu logga in. - - - Lägg till objekt - - - Apptillägg - - - Använd Bitwardens tillgänglighetstjänst för att automatiskt fylla i dina inloggningar i applikationer och på webben. - - - Hjälpmedelsservice för automatisk ifyllnad - - - Undvik tvetydiga tecken - - - Bitwarden apptillägg - - - Det enklaste sättet att lägga till nya inloggningar i ditt valv är genom apptillägget. Läs mer om Bitwardens apptillägg genom att navigera till "Verktyg"-skärmen. - - - Använd Bitwarden i Safari och andra applikationer för att automatiskt fylla i din inloggningsinformation. - - - Bitwardens hjälpmedelsservice för automatisk ifyllnad - - - Använd Bitwardens tillgänglighetstjänst för att automatiskt fylla i din inloggningsinformation. - - - Ändra e-postadress - - - Du kan ändra din e-postadress på bitwardens webbvalv. Vill du besöka webbplatsen nu? - - - Ändra huvudlösenord - - - Du kan ändra ditt huvudlösenord på bitwardens webbvalv. Vill du besöka webbplatsen nu? - - - Stäng - - - Kommer snart! - - - Fortsätt - - - Kopierat! - - - Kopierat lösenord! - - - Kopierat användarnamn! - - - Skapa konto - - - Skapar konto... - Message shown when interacting with the server - - - Redigera objekt - - - Aktivera automatisk synkronisering - - - Ange ditt kontos e-postadress för att hämta din huvudlösenordsledtråd. - - - Återaktivera apptillägg - - - Nästan färdigt! - - - Aktivera apptillägg - - - Inuti Safari hittar du Bitwarden genom att trycka på dela-ikonen (scrolla till höger på den nedersta raden i dela-menyn). - Safari is the name of apple's web browser - - - Få omedelbar åtkomst till dina lösenord! - - - Du är nu redo att logga in! - - - Visa appar som stöds - - - Dina inloggningar är nu enkelt tillgängliga från Safari, Chrome och andra appar som stöds. - - - Inuti Safari och Chrome hittar du Bitwarden genom att trycka på dela-ikonen (hjälp: skrolla till höger på den nedersta raden i dela-menyn). - - - Tryck på Bitwarden-ikonen i menyn för att öppna tillägget. - - - För att starta Bitwarden i Safari och andra appar, tryck på "mer"-ikonen i den nedersta raden på menyn. - - - Favorit - - - Fingeravtryck - - - Skapa lösenord - - - Hämta huvudlösenordsledtråd - - - Importera objekt - - - Du kan massimportera objekt på bitwardens webbvalv. Vill du besöka webbsidan nu? - - - Massimportera dina objekt snabbt från andra lösenordshanterare. - - - Senaste synkning: - - - Längd - - - Lås - - - 15 minuter - - - 1 timme - - - 1 minut - - - 4 timmar - - - Omedelbart - - - Låsalternativ - - - Loggar in... - Message shown when interacting with the server - - - Logga in eller skapa ett nytt konto för att komma åt dina lösenord. - - - Hantera - - - Bekräftelsen för huvudlösenordet stämde ej. - - - Huvudlösenordet är det lösenord som du använder för att komma åt ditt valv. Det är väldigt viktigt att du inte glömmer bort ditt huvudlösenord, eftersom det inte går att återställa lösenordet om du skulle glömma bort det. - - - Huvudlösenordsledtråd (frivillig) - - - En huvudlösenordsledtråd kan hjälpa dig att komma ihåg ditt huvudlösenord om du glömmer bort det. - - - Huvudlösenordet måste vara minst 8 tecken långt. - - - Minst antal nummer - Minimum numeric characters for password generator settings - - - Minst antal speciella tecken - Minimum special characters for password generator settings - - - Fler inställningar - - - Du måste logga in i Bitwarden-appen innan du kan använda tillägget. - - - Aldrig - - - Nytt objekt skapat. - - - Det finns inga favoriter i ditt valv. - - - Det finns inga objekt i ditt valv. - - - Det finns inga objekt i ditt valv för den här webbplatsen. Tryck för att skapa ett. - - - Den här inloggningen har inte ett användarnamn eller lösenord angivet. - - - Ok, fattar! - Confirmation, like "Ok, I understand it" - - - Standardinställningar ställs in genom Bitwarden-appens lösenordsgenerator-verktyg. - - - Alternativ - - - Annat - - - Lösenord skapat. - - - Lösenordsgenerator - - - Lösenordsledtråd - - - Vi har skickat ett mejl till dig med din huvudlösenordsledtråd. - - - Är du säker på att du vill skriva över det nuvarande lösenordet? - - - Bitwarden håller ditt valv uppdaterat automatiskt genom att använda push-aviseringar. För den bästa möjliga upplevelsen vänligen välj "Tillåt" på förfrågan om du vill aktivera push-aviseringar. - Push notifications for apple products - - - Betygsätt appen - - - Överväg gärna att hjälpa oss genom att ge oss en bra recension! - - - App Store-betyg återställs för varje ny version av Bitwarden. Överväg gärna att hjälpa oss genom att ge oss en bra recension! - - - Skapa nytt lösenord - - - Ange huvudlösenordet igen - - - Sök i valvet - - - Säkerhet - - - Se utvecklingsframsteg - - - Välj - - - Ställ in PIN-kod - - - Ange en fyrsiffrig PIN-kod som du låser upp appen med. - - - Objektinformation - - - Objekt uppdaterat. - - - Skickar... - Message shown when interacting with the server - - - Synkroniserar... - Message shown when interacting with the server - - - Synkronisering genomförd. - - - Synkronisering misslyckades. - - - Synka valvet nu - - - Touch ID - What Apple calls their fingerprint reader. - - - Tvåstegsverifiering - - - Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, till exempel genom en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller mejl. Tvåstegsverifiering kan aktiveras på bitwardens webbvalv. Vill du besöka webbplatsen nu? - - - Lås upp med {0} - - - Lås upp med PIN-kod - - - Validerar - Message shown when interacting with the server - - - Verifieringskod - - - Visa objekt - - - Bitwarden webbvalv - - - Hantera dina inloggningar från valfri webbläsare med Bitwardens webbvalv. - - - Förlorat autentiseringsapp? - - - Objekt - Screen title - - - Tillägg aktiverat! - - - Ikoner - - - Översättningar - - - Objekt för {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Det finns inga objekt i ditt valv för {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - När du ser en avisering från Bitwarden för automatisk ifyllnad så kan du trycka på den för att öppna tjänsten. - - - Tryck på den här aviseringen för att automatiskt fylla i en inloggning från ditt valv. - - - Öppna tillgänglighetsinställningar - - - 1. I Androids tillgänglighetsinställningar trycker du på "Bitwarden" under tjänster. - - - 2. Växla till på-läget och tryck på OK för att acceptera. - - - Inaktiverad - - - Aktiverad - - - Status - - - Beta - - - Det lättaste sättet att lägga till nya inloggningar till ditt valv är från Bitwardens hjälpmedelsservice för automatisk ifyllnad. Läs mer om hur man använder Bitwardens hjälpmedelsservice för automatisk ifyllnad genom att navigera till "Verktyg"-skärmen. - - - Automatisk ifyllnad - - - Vill du automatiskt fylla i den här inloggningen eller visa den? - - - Är du säker på att du vill automatiskt fylla i den här inloggningen? Det är inte en fullständig träff för "{0}". - - - Matchande objekt - - - Möjliga matchande objekt - - - Sök - - - Du letar efter en inloggning som kan automatiskt fyllas i för "{0}". - - - Dela ditt valv - - - Skapa en organisation för att på ett säkert sätt kunna dela dina inloggningar med andra användare. - - - Skanna när ett lösenordsfält är fokuserat - - - Skanna endast efter fält och erbjud en avisering för automatisk ifyllnad av en inloggning när du väljer ett lösenordsfält. Den här inställningen kan hjälpa till att spara på batteriet. - - - Permanent avisering - - - Erbjud alltid att automatisk fylla i en inloggning med en bestående avisering och skanna endast fält vid automatisk ifyllnad. Den här inställningen kan hjälpa till att spara på batteriet. - - - Skanna alltid - - - Skanna alltid efter fält och erbjud endast en avisering för att automatiskt fylla i en inloggning om lösenordsfält hittades. Detta är standardinställningen. - - - Kan inte öppna appen "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Autentiseringsapp - For 2FA - - - Ange den 6-siffriga verifieringskoden från din autentiseringsapp. - For 2FA - - - Ange den 6-siffriga verifieringskoden som skickades till {0}. - For 2FA - - - Inloggning inte tillgänglig - For 2FA whenever there are no available providers on this device. - - - Detta konto har tvåstegsverifiering aktiverat, men inget av alternativen som används stöds på den här enheten. Använd en enhet som stöds och/eller lägg till en metod för tvåstegsverifiering med bättre stöd för flera olika typer av enheter (t.ex. via en autentiseringsapp). - - - Återställningskod - For 2FA - - - Kom ihåg mig - Remember my two-step login - - - Skicka verifieringskod-mejlet igen - For 2FA - - - Alternativ för tvåstegsverifiering - - - Använd en annan inloggningsmetod för tvåstegsverifiering - - - Kunde inte skicka verifierings-mejl. Försök igen. - For 2FA - - - Verifierings-mejl skickat. - For 2FA - - - För att fortsätta, håll din YubiKey NEO mot baksidan av enheten eller sätt i din YubiKey i USB-porten på din enhet, rör sedan knappen. - - - YubiKey säkerhetsnyckel - "YubiKey" is the product name and should not be translated. - - - Bifoga fil - - - Bifogade filer - - - Kunde ej hämta fil. - - - Enheten kan inte öppna denna typ av fil. - - - Laddar ner... - Message shown when downloading a file - - - Denna bilaga är {0} i storlek. Är du säker på att du vill ladda ner den till din enhet? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Autentiseringsnyckel (TOTP) - - - Verifieringskod (TOTP) - Totp code label - - - Autentiseringsnyckel tillagd. - - - Kan inte läsa autentiseringsnyckeln. - - - Skanningen kommer att ske automatiskt. - - - Rikta kameran mot en QR-kod. - - - Skanna QR-kod - - - Kamera - - - Foton - - - Kopierade TOTP! - - - Kopiera TOTP - - - Om din inloggning har en autentiseringsnyckel kopplad till den, kommer TOTP-verifieringskoden att automatiskt kopieras till urklipp när du automatiskt fyller i inloggningen. - - - Inaktivera automatisk TOTP-kopiering - - - Ett premium-medlemskap krävs för att använda den här funktionen. - - - Bilaga tillagd - - - Bilaga borttagen - - - Välj fil - - - Fil - - - Ingen fil vald - - - Det finns inga bilagor. - - - Filkälla - - - Funktionen är inte tillgänglig - - - Filen får vara maximalt 100 MB. - - - Du kan inte använda denna funktion förrän du uppdaterar din krypteringsnyckel. - - - Läs mer - - - API server-URL - - - Anpassad miljö - - - För avancerade användare. Du kan ange bas-URL:en för varje tjänst oberoende av varandra. - - - Miljö-URL:er har sparats. - - - {0} är inte korrekt formaterad. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Identitetsserver-URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Egen-hostad miljö - - - Ange bas-URL:en för din "on-premise"-hostade Bitwarden-installation. - - - Server-URL - - - Webbvalv server-URL - - - Tryck på den här aviseringen för att visa inloggningar från ditt valv. - - - Anpassade fält - - - Kopiera nummer - - - Kopiera säkerhetskod - - - Nummer - - - Säkerhetskod - - - Vilken typ av objekt vill du lägga till? - - - Kort - - - Identitet - - - Inloggning - - - Säker anteckning - - - Adress 1 - - - Adress 2 - - - Adress 3 - - - April - - - Augusti - - - Märke - - - Kortinnehavarens namn - - - Ort - - - Företag - - - Land - - - December - - - Dr - - - Förfallomånad - - - Förfalloår - - - Februari - - - Förnamn - - - Januari - - - Juli - - - Juni - - - Efternamn - - - Licensnummer - - - Mars - - - Maj - - - Mellannamn - - - Herr - - - Fru - - - Fröken - - - November - - - Oktober - - - Passnummer - - - Telefon - - - September - - - Personnummer - - - Län - - - Titel - - - Postnummer - - - Adress - - - Utgång - - - Inaktivera webbplatsikoner - - - Webbplatsikoner ger en igenkännbar ikon bredvid varje inloggningsobjekt i ditt valv. - - - Ikoner Server-URL - - - Fyll i automatiskt med Bitwarden - - - Valvet är låst - - - Gå till mitt valv - - - Samlingar - - - Det finns inga objekt i samlingen. - - - Det finns inga objekt i den här mappen. - - - Tillgänglighetstjänst för automatisk ifyllnad - - - Bitwardens tillgänglighetstjänst för automatisk ifyllnad använder Android Autofill Framework för att assistera med att fylla i inloggningar, kreditkort och identitetsinformation inuti andra appar på din enhet. - - - Använd Bitwardens tillgänglighetstjänst för att automatiskt fylla i inloggningar, kreditkort och identitetsinformation inuti andra appar. - - - Öppna inställningar för automatisk ifyllnad - - - Face ID - What Apple calls their facial recognition reader. - - - Använd Face ID för att verifiera. - - - Använd Face ID för att låsa upp - - - Verifiera Face ID - - - Lås upp med Windows Hello - - - Verifiera med Windows Hello - - - Windows Hello - - - Det gick inte att automatiskt öppna inställningsmenyn för Android automatisk ifyllnad. Du kan navigera till menyn manuellt från Android inställningar > System > Språk och inmatning > Avancerat > Tjänsten autofyll. - - - Anpassat fältnamn - - - Boolean - - - Dold - - - Text - - - Nytt anpassat fält - - - Vilken typ av anpassat fält vill du lägga till? - - - Ta bort - - - Ny URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Basdomän - - - Standard - - - Exakt - - - Värd - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Reguljärt uttryck - A programming term, also known as 'RegEx'. - - - Börjar med - - - Matchning av URI - - - Matchning - URI match detection for auto-fill. - - - Ja, och spara - - - Fyll i automatiskt och spara - - - Organisation - An entity of multiple related people (ex. a team or business organization). - - - Håll din Yubikey nära ovansidan av enheten. - - - Försök igen - - - För att fortsätta, håll din YubiKey NEO mot baksidan av enheten. - - - Tillgänglighetstjänsten kan vara användbar när appar inte stöder standardvarianten av ifyllnad. - - - Lösenord uppdaterat - ex. Date this password was updated - - - Uppdaterad - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.th.Designer.cs b/src/App/Resources/AppResources.th.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.th.resx b/src/App/Resources/AppResources.th.resx deleted file mode 100644 index 0b8b37c14..000000000 --- a/src/App/Resources/AppResources.th.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - เกี่ยวกับ - - - เพิ่ม - Add/create a new entity (verb). - - - เพิ่มโฟลเดอร์ - - - เพิ่มรายการ - The title for the add item page. - - - พบข้อผิดพลาด - Alert title when something goes wrong. - - - ย้อนกลับ - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - ยกเลิก - Cancel an operation. - - - คัดลอก - Copy some value to your clipboard. - - - คัดลอกรหัสผ่าน - The button text that allows a user to copy the login's password to their clipboard. - - - คัดลอกชื่อผู้ใช้ - The button text that allows a user to copy the login's username to their clipboard. - - - เครดิต - Title for page that we use to give credit to resources that we use. - - - ลบ - Delete an entity (verb). - - - กำลังลบ... - Message shown when interacting with the server - - - คุณแน่ใจหรือไม่ว่าต้องการลบ? การกระทำนี้ไม่สามารถเลิกทำได้ - Confirmation alert message when deleteing something. - - - แก้ไข - - - แก้ไขโฟลเดอร์ - - - อีเมล - Short label for an email address. - - - ที่อยู่อีเมล์ - Full label for a email address. - - - ส่งอีเมลถึงเรา - - - ส่งอีเมลถึงเราโดยตรงได้เพื่อรับความช่วยเหลือ หรือแสดงความคิดเห็น - - - ป้อนรหัส PIN ของคุณ - - - รายการโปรด - Title for your favorite items in the vault. - - - รายงานข้อบกพร่อง - - - เปิดประเด็นที่เก็บ GitHub ของเรา - - - ใช้ลายนิ้วมือเพื่อตรวจสอบ - - - โฟลเดอร์ - Label for a folder. - - - สร้างโฟลเดอร์ใหม่แล้ว - - - ลบโฟลเดอร์แล้ว. - - - ไม่มีโฟลเดอร์ - Items that have no folder specified go in this special "catch-all" folder. - - - โฟลเดอร์ - - - ปรับปรุงโฟลเดอร์แล้ว - - - ไปที่เว็บไซต์ - The button text that allows user to launch the website to their web browser. - - - ช่วยเหลือ & ข้อเสนอแนะ - - - ซ่อน - Hide a secret value that is currently shown (password). - - - โปรดเชื่อมต่ออินเทอร์เน็ตก่อนดำเนินการต่อ - Description message for the alert when internet connection is required to continue. - - - ต้องการการเชื่อมต่ออินเทอร์เน็ต - Title for the alert when internet connection is required to continue. - - - รหัสผ่านหลักไม่ถูกต้อง ลองอีกครั้ง - - - PIN ไม่ถูกต้อง ลองอีกครั้ง - - - เปิด - The button text that allows user to launch the website to their web browser. - - - เข้าสู่ระบบ - The login button text (verb). - - - เข้าสู่ระบบ - Title for login page. (noun) - - - ออกจากระบบ - The log out button text (verb). - - - คุณแน่ใจว่าต้องการออกจากระบบ - - - รหัสผ่านหลัก - Label for a master password. - - - เพิ่มเติม - Text to define that there are more options things to see. - - - ห้องนิรภัยของฉัน - The title for the vault page. - - - ชื่อ - Label for an entity name. - - - ไม่ใช่ - - - หมายเหตุ - Label for notes. - - - ตกลง - Acknowledgement. - - - รหัสผ่าน - Label for a password. - - - บันทึก - Button text for a save operation (verb). - - - กำลังบันทึก... - Message shown when interacting with the server - - - การตั้งค่า - The title for the settings page. - - - แสดง - Reveal a hidden value (password). - - - รายการถูกลบแล้ว - Confirmation message after successfully deleting a login. - - - ส่งข้อมูล - - - การซิงค์ - The title for the sync page. - - - ขอบคุณ - - - เครื่องมือ - The title for the tools page. - - - URI - Label for a uri/url. - - - ใช้ลายนิ้วมือเพื่อปลดล็อก - - - ชื่อผู้ใช้ - Label for a username. - - - เขตข้อมูล {0} จำเป็นต้องระบุ - Validation message for when a form field is left blank and is required to be entered. - - - {0} ถูกคัดลอกแล้ว - Confirmation message after suceessfully copying a value to the clipboard. - - - ยืนยันลายนิ้วมือ - - - ยืนยันรหัสผ่านหลัก - - - ยืนยัน PIN - - - เวอร์ชัน - - - แสดง - - - เยี่ยมชมเว็บไซต์ - - - Visit our website to get help, news, email us, and/or learn more about how to use bitwarden. - - - เว็บไซต์ - Label for a website. - - - ใช่ - - - บัญชี - - - บัญชีของคุณได้เปิดใช้แล้ว. ตอนนี้คุณสามารถเข้าสู่ระบบได้แล้ว. - - - เพิ่มรายการ - - - แอปส่วนขยาย - - - Use the bitwarden accessibility service to auto-fill your logins across apps and the web. - - - บริการกรอกข้อมูลอัตโนมัติ - - - หลีกเลี่ยงอักขระที่ไม่ชัดเจน - - - ส่วนขยายแอปพลิเคชัน bitwarden - - - The easiest way to add new logins to your vault is from the bitwarden App Extension. Learn more about using the bitwarden App Extension by navigating to the "Tools" screen. - - - Use bitwarden in Safari and other apps to auto-fill your logins. - - - bitwarden Auto-fill Service - - - Use the bitwarden accessibility service to auto-fill your logins. - - - เปลี่ยนอีเมล - - - คุณสามารถเปลี่ยนที่อยู่อีเมลของคุณบน เว็บนิรภัย bitwarden.com คุณต้องการเยี่ยมชมเว็บไซต์เดี๋ยวนี้หรือไม่ - - - เปลี่ยนรหัสผ่านหลัก - - - คุณสามารถเปลี่ยนรหัสผ่านหลักของคุณบน เว็บนิรภัย bitwarden.com คุณต้องการเยี่ยมชมเว็บไซต์เดี๋ยวนี้หรือไม่ - - - ปิด - - - เร็วๆ นี้! - - - ดำเนินการต่อไป - - - Copied! - - - คัดลอกรหัสผ่านแล้ว! - - - ชื่อผู้ใช้คัดลอกแล้ว! - - - สร้างบัญชี - - - กำลังสร้างบัญชี... - Message shown when interacting with the server - - - แก้ไขรายการ - - - เปิดใช้การซิงค์อัตโนมัติ - - - ป้อนที่อยู่อีเมลบัญชีของคุณเพื่อรับคำแนะนำเกี่ยวกับรหัสผ่านหลักของคุณ - - - เปิดใช้งานส่วนขยายแอปอีกครั้ง - - - เกือบเสร็จแล้ว! - - - เปิดใช้งานแอปส่วนขยาย - - - In Safari, find bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu). - Safari is the name of apple's web browser - - - รับสิทธิ์เข้าถึงรหัสผ่านของคุณได้ทันที! - - - คุณพร้อมที่จะเข้าระบบแล้ว! - - - ดูแอปที่สนับสนุน - - - ขณะนี้คุณสามารถเข้าสู่ระบบอยู่ในได้อย่างง่ายดายจาก Safari, Chrome และปพลิเคชันอื่นๆที่ได้รับการสนับสนุน - - - In Safari and Chrome, find bitwarden using the share icon (hint: scroll to the right on the bottom row of the share menu). - - - Tap the bitwarden icon in the menu to launch the extension. - - - To turn on bitwarden in Safari and other apps, tap the "more" icon on the bottom row of the menu. - - - รายการโปรด - - - ลายนิ้วมือ - - - สร้างรหัสผ่าน - - - รับคำใบ้เกี่ยวกับรหัสผ่านหลักของคุณ - - - นำเข้ารายการ - - - คุณสามารถนำเข้ารายการจำนวนมากจาก เว็บนิรภัย bitwarden.com คุณต้องการเยี่ยมชมเว็บไซต์เดี๋ยวนี้หรือไม่ - - - นำเข้ารายการจำนวนมากจากรายการจัดการรหัสผ่านอื่น ๆ ได้อย่างรวดเร็ว - - - การซิงค์ครั้งล่าสุด: - - - ความยาว - - - ล็อค - - - 15 นาที - - - 1 ชั่วโมง - - - 1 นาที - - - 4 ชั่วโมง - - - ทันที - - - ตัวเลือกการล็อก - - - กำลังเข้าสู่ระบบ... - Message shown when interacting with the server - - - ลงชื่อเข้าใช้หรือสร้างบัญชีใหม่เพื่อเข้าสู่ห้องนิรภัยที่ปลอดภัยของคุณ - - - จัดการ - - - การยืนยันรหัสผ่านไม่ถูกต้อง - - - รหัสผ่านหลักคือรหัสผ่านที่คุณใช้เพื่อเข้าถึงห้องนิรภัยของคุณ เป็นเรื่องสำคัญมากที่คุณจะไม่ลืมรหัสผ่านหลักของคุณ ไม่มีวิธีการกู้คืนรหัสผ่านในกรณีที่คุณลืมรหัสผ่าน - - - คำใบ้รหัสผ่านหลัก (ไม่จำเป็น) - - - คำแนะนำเกี่ยวกับรหัสผ่านหลักสามารถช่วยให้คุณจดจำรหัสผ่านได้หากลืม - - - รหัสผ่านหลักต้องมีความยาวอย่างน้อย8อักขระ - - - จำนวนตัวเลขต่ำสุด - Minimum numeric characters for password generator settings - - - จำนวนตัวพิเศษขั้นต่ำ - Minimum special characters for password generator settings - - - การตั้งค่าเพิ่มเติม - - - You must log into the main bitwarden app before you can use the extension. - - - ไม่เคย - - - สร้างรายการใหม่แล้ว - - - ไม่มีรายการโปรดในห้องนิรภัยของคุณ - - - ไม่มีรายการในห้องนิรภัยของคุณ - - - ไม่มีรายการอยู่ในห้องนิรภัยของคุณสำหรับเว็บไซต์นี้ แตะเพื่อเพิ่ม - - - การเข้าสู่ระบบนี้ไม่มีการตั้งค่าชื่อผู้ใช้หรือรหัสผ่าน - - - ตกลง, เข้าใจแล้ว - Confirmation, like "Ok, I understand it" - - - Option defaults are set from the main bitwarden app's password generator tool. - - - ตัวเลือก - - - อื่น ๆ - - - ตัวสร้างรหัสผ่าน - - - ตัวสร้างรหัสผ่าน - - - คำใบ้รหัสผ่าน - - - เราได้ส่งอีเมลที่ มีคำใบ้รหัสผ่านหลักของคุณแล้ว - - - คุณแน่ใจว่าต้องการเขียนทับรหัสผ่านปัจจุบัน? - - - bitwarden keeps your vault automatically synced by using push notifications. For the best possible experience, please select "Ok" on the following prompt when asked to enable push notifications. - Push notifications for apple products - - - ให้คะแนนแอพนี้ - - - โปรดพิจารณาช่วยเหลือเราด้วยความคิดเห็นที่ดี! - - - App Store ratings are reset with every new version of bitwarden. Please consider helping us out with a good review! - - - สร้างรหัสผ่านใหม่ - - - ยืนยันรหัสผ่านหลัก - - - Search vault - - - ความปลอดภัย - - - ดูความคืบหน้าในการพัฒนา - - - เลือก - - - ตั้งรหัส PIN - - - ป้อนรหัส PIN 4 หลักเพื่อปลดล็อคแอพพลิเคชั่น - - - ข้อมูลรายการ - - - ปรับปรุงรายการการแล้ว - - - กำลังส่ง... - Message shown when interacting with the server - - - กำลังซิงค์... - Message shown when interacting with the server - - - ทำการซิงค์เสร็จสมบูรณ์ - - - ทำการซิงค์ล้มเหลว - - - ซิงค์ตู้นิรภัยเดี๋ยวนี้ - - - Touch ID - What Apple calls their fingerprint reader. - - - เข้าสู่ระบบแบบสองขั้นตอน - - - การเข้าสู่ระบบแบบสองขั้นตอนทำให้บัญชีของคุณมีความปลอดภัยมากขึ้นด้วยการให้คุณตรวจสอบการเข้าสู่ระบบของคุณกับอุปกรณ์อื่นเช่นคีย์ความปลอดภัย, แอพ authenticator, SMS, โทรศัพท์หรืออีเมล. เข้าสู่ระบบแบบสองขั้นตอนสามารถเปิดใช้งานบน เว็บนิรภัย bitwarden.com คุณต้องการเยี่ยมชมเว็บไซต์เดี๋ยวนี้หรือไม่ - - - ปลดล็อก ด้วย {0} - - - ปลดล็อก ด้วยรหัส PIN - - - กำลังตรวจสอบ... - Message shown when interacting with the server - - - รหัสยืนยัน - - - ดูรายการ - - - bitwarden Web Vault - - - จัดการรายการของคุณจากเว็บเบราเซอร์ใดๆกับ เว็บนิรภัย bitwarden - - - แอปตรวจสอบการยืนยันหายไป? - - - รายการ - Screen title - - - เปิดใช้งานส่วนขยายแล้ว! - - - ไอคอน - - - การแปลภาษา - - - รายการสำหรับ {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - ไม่มีรายการ {0} ในห้องนิรภัยของคุณ - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - When you see a bitwarden auto-fill notification, you can tap it to launch the auto-fill service. - - - Tap this notification to auto-fill a login from your vault. - - - เปิดการตั้งค่าการเข้าถึง - - - 1. On the Android Accessibility Settings screen, touch "bitwarden" under the Services heading. - - - 2. สลับเปิดสวิตช์และกดตกลงเพื่อยอมรับ - - - ปิดการใช้งาน - - - เปิดใช้งาน - - - สถานะ - - - เบต้า - - - The easiest way to add new logins to your vault is from the bitwarden Auto-fill Service. Learn more about using the bitwarden Auto-fill Service by navigating to the "Tools" screen. - - - กรอกข้อมูลอัตโนมัติ - - - คุณต้องการกรอกข้อมูลอัตโนมัติหรือดูรายการนี้หรือไม่? - - - Are you sure you want to auto-fill this login? It is not a complete match for "{0}". - - - รายการที่ตรงกัน - - - รายการที่ตรงกันที่เป็นไปได้ - - - ค้นหา - - - You are searching for an auto-fill login for "{0}". - - - แบ่งปันห้องนิรภัยของคุณ - - - Create an organization to securely share your logins with other users. - - - สแกนเมื่อเน้นช่องรหัสผ่าน - - - เพียงสแกนหน้าจอสำหรับเขตข้อมูล และมีการแจ้งเตือนการกรอกข้อมูลอัตโนมัติเมื่อคุณเลือกเขตข้อมูลรหัสผ่าน การตั้งค่านี้อาจช่วยประหยัดแบตเตอรี่ - - - การแจ้งเตือนแบบคงที่ - - - มีการแจ้งให้ทราบโดยอัตโนมัติเสมอและสแกนเฉพาะเขตข้อมูลหลังจากพยายามเติมอัตโนมัติ การตั้งค่านี้อาจช่วยประหยัดอายุการใช้งานแบตเตอรี่ - - - สแกนเสมอ - - - จะสแกนหน้าจอสำหรับเขตข้อมูลเท่านั้น และ ให้แจ้งการกรอกข้อมูลอัตโนมัติหากพบเขตข้อมูลรหัสผ่าน นี้เป็นค่าเริ่มต้น - - - ไม่สามารถเปิดแอป "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - แอป Authenticator - For 2FA - - - ป้อนรหัสยืนยัน 6 หลักจากคุณแอป authenticator - For 2FA - - - ป้อนรหัสยืนยัน6หลักที่ส่งอีเมลไปที่ {0} - For 2FA - - - เข้าสู่ระบบไม่พร้อมใช้งาน - For 2FA whenever there are no available providers on this device. - - - บัญชีนี้มีการเปิดใช้งานการเข้าสู่ระบบสองขั้นตอนอย่างไรก็ตามไม่มีการกำหนดค่าผู้ให้บริการสองขั้นตอนที่ได้รับการสนับสนุนบนอุปกรณ์นี้ โปรดใช้อุปกรณ์ที่รองรับและ/หรือเพิ่มผู้ให้บริการเพิ่มเติมที่ได้รับการสนับสนุนให้ดีขึ้นในอุปกรณ์ต่างๆ (เช่นแอป authenticator) - - - รหัสกู้คืน - For 2FA - - - จำการเข้าระบบของฉันไว้ - Remember my two-step login - - - ส่งอีเมล์ยืนยันรหัสอีกครั้ง - For 2FA - - - ตัวเลือกการเข้าสู่ระบบแบบสองขั้นตอน - - - ใช้วิธีลงชื่อเข้าใช้แบบสองขั้นตอนวิธีอื่น - - - ไม่สามารถส่งอีเมลการตรวจสอบได้ ลองอีกครั้ง - For 2FA - - - ส่งอีเมลยืนยันแล้ว - For 2FA - - - Hold your YubiKey NEO against the back of the device to continue. - - - YubiKey NEO Security Key - "YubiKey" is the product name and should not be translated. - - - เพิ่มไฟล์แนบใหม่ - - - สิ่งที่แนบมา - - - ไม่สามารถโหลดไฟล์ได้ - - - อุปกรณ์ของคุณไม่สามารถเปิดแฟ้มชนิดนี้ได้ - - - กำลังดาวน์โหลด... - Message shown when downloading a file - - - ไฟล์แนบนี้มีขนาด {0} คุณแน่ใจหรือไม่ว่าต้องการดาวน์โหลดลงในอุปกรณ์ของคุณ? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - คีย์ Authenticator (TOTP) - - - รหัสยืนยัน (TOTP) - Totp code label - - - คีย์ Authenticator ที่เพิ่ม - - - ไม่สามารถอ่านคีย์ authenticator ได้ - - - การสแกนจะเกิดโดยอัตโนมัติ - - - เล็งกล้องไปที่รหัส QR - - - สแกน QR Code - - - กล้องถ่ายรูป - - - รูปภาพ - - - คัดลอก TOTP แล้ว! - - - คัดลอก TOTP - - - ถ้าการเข้าสู่ระบบของคุณมีคีย์การรับรองความถูกต้องที่แนบอยู่กับรหัสยืนยัน TOTP จะถูกคัดลอกไปยังคลิปบอร์ดของคุณโดยอัตโนมัติเมื่อคุณกรอกข้อมูลเข้าสู่ระบบโดยอัตโนมัติ - - - ปิดใช้งานการคัดลอก TOTP โดยอัตโนมัติ - - - จำเป็นต้องมีสมาชิกระดับพรีเมียมเพื่อใช้คุณลักษณะนี้ - - - เพิ่มสิ่งที่แนบมา - - - ลบสิ่งที่แนบมา - - - เลือกไฟล์ - - - ไฟล์ - - - ไม่มีไฟล์ที่เลือก - - - ไม่มีสิ่งที่แนบมา - - - แหล่งที่มาของไฟล์ - - - ไม่สามารถใช้คุณสมบัตินี้ได้ - - - ขนาดไฟล์สูงสุดคือ 100 MB - - - คุณไม่สามารถใช้คุณลักษณะนี้ได้จนกว่าคุณปรับปรุงคีย์การเข้ารหัสลับของคุณ - - - เรียน​รู้​เพิ่ม​เติม - - - URL เซิร์ฟเวอร์ API - - - สภาพแวดล้อมแบบกำหนดเอง - - - สำหรับผู้ใช้ขั้นสูง คุณสามารถระบุ URL พื้นฐานของแต่ละบริการได้อย่างอิสระ - - - มีการบันทึก URL ของสภาพแวดล้อมแล้ว - - - {0} ไม่ได้จัดรูปแบบอย่างถูกต้อง - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL เซิร์ฟเวอร์ข้อมูลประจำตัว - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - จัดสภาพแวดล้อมโฮสต์ด้วยตนเอง - - - Specify the base URL of your on-premise hosted bitwarden installation. - - - URL ของเซิร์ฟเวอร์ - - - URL เซิร์ฟเวอร์ของเว็บนิรภัย - - - แตะการแจ้งเตือนนี้เพื่อดูรายการจากห้องนิรภัยของคุณ - - - เขตข้อมูลแบบระบุเอง - - - คัดลอกหมายเลข - - - คัดลอกรหัสรักษาความปลอดภัย - - - หมายเลข - - - รหัสความปลอดภัย - - - คุณต้องการเพิ่มประเภทรายการใด? - - - บัตร - - - ข้อมูลระบุตัวตน - - - เข้าสู่ระบบ - - - บันทึกการรักษาปลอดภัย - - - ที่อยู่ 1 - - - ที่อยู่ 2 - - - ที่อยู่ 3 - - - เมษายน - - - สิงหาคม - - - แบรนด์ - - - ชื่อผู้ถือบัตร - - - เมือง - - - บริษัท - - - ประเทศ - - - ธันวาคม - - - ดร. - - - เดือนที่หมดอายุ - - - ปีที่หมดอายุ - - - กุมภาพันธ์ - - - ชื่อจริง - - - มกราคม - - - กรกฎาคม - - - มิถุนายน - - - นามสกุล - - - หมายเลขใบอนุญาต - - - มีนาคม - - - พฤษภาคม - - - ชื่อกลาง - - - นาย - - - นาง - - - นางสาว - - - พฤศจิกายน - - - ตุลาคม - - - หมายเลขหนังสือเดินทาง - - - โทรศัพท์ - - - กันยายน - - - หมายเลขประกันสังคม - - - รัฐ / จังหวัด - - - คำนำหน้า - - - รหัสไปรษณีย์ - - - ที่อยู่ - - - วันหมดอายุ - - - ปิดใช้งานไอคอนเว็บไซต์ - - - ไอคอนเว็บไซต์จะให้ภาพที่รู้จักถัดจากแต่ละรายการเข้าสู่ระบบในห้องนิรภัยของคุณ - - - URL ของเซิร์ฟเวอร์ไอคอน - - - Auto-fill with bitwarden - - - ห้องนิรภัยถูกล็อก - - - ไปที่ห้องนิรภัยของฉัน - - - คอลเลกชัน - - - ไม่มีรายการในคอลเล็กชันนี้ - - - ไม่มีรายการในโฟลเดอร์นี้ - - - เติมข้อมูลบริการการเข้าถึงได้โดยอัตโนมัติ - - - The bitwarden auto-fill service uses the Android Autofill Framework to assist in filling logins, credit cards, and identity information into other apps on your device. - - - Use the bitwarden accessibility service to auto-fill your logins. - - - เปิดการตั้งค่าการป้อนอัตโนมัติ - - - Face ID - What Apple calls their facial recognition reader. - - - ใช้ Face ID เพื่อตรวจสอบ - - - ใช้ Face ID เพื่อปลดล็อก - - - ยืนยันรหัส Face ID - - - ปลดล็อก ด้วย Windows Hello - - - ยืนยันด้วย Windows Hello - - - Windows Hello - - - ไม่สามารถเปิดเมนูการตั้งค่าการป้อนอัตโนมัติของ Android ให้คุณได้โดยอัตโนมัติ, คุณสามารถไปที่เมนูการตั้งค่าการป้อนข้อมูลอัตโนมัติด้วยตนเองได้จากการตั้งค่าของ Android > ระบบ > ภาษาด้วยตนเองและป้อนข้อมูล > ขั้นสูง > บริการป้อนข้อความอัตโนมัติ - - - ชื่อเขตข้อมูล ที่กำหนดเอง - - - ค่าบูลีน - - - ซ่อน - - - ข้อความ - - - สร้างเขตข้อมูลที่กำหนดเองใหม่ - - - คุณต้องการเพิ่มเขตข้อมูลแบบกำหนดเองชนิดใด - - - เอาออก - - - URI ใหม่ - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - โดเมนพื้นฐาน - - - ค่าเริ่มต้น - - - ถูกต้อง - - - โฮสต์ - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - นิพจน์ทั่วไป - A programming term, also known as 'RegEx'. - - - เริ่มต้นด้วย - - - ตรวจพบการจับคู่ URL - - - ตรวจพบการจับคู่ - URI match detection for auto-fill. - - - Yes, and Save - - - Auto-fill and save - - - Organization - An entity of multiple related people (ex. a team or business organization). - - - Hold your Yubikey near the top of the device. - - - Try Again - - - To continue, hold your YubiKey NEO against the back of the device. - - - The accessibility service may be helpful to use when apps do not support the standard auto-fill service. - - - Password Updated - ex. Date this password was updated - - - Updated - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.tr.Designer.cs b/src/App/Resources/AppResources.tr.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.tr.resx b/src/App/Resources/AppResources.tr.resx deleted file mode 100644 index 0ee065f58..000000000 --- a/src/App/Resources/AppResources.tr.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Hakkında - - - Ekle - Add/create a new entity (verb). - - - Klasör Ekle - - - Hesap Ekle - The title for the add item page. - - - Bir hata oluştu. - Alert title when something goes wrong. - - - Geri - Navigate back to the previous screen. - - - bitwarden - App name. Shouldn't ever change. - - - İptal - Cancel an operation. - - - Kopyala - Copy some value to your clipboard. - - - Parolayı Kopyala - The button text that allows a user to copy the login's password to their clipboard. - - - Kullanıcı Adını Kopyala - The button text that allows a user to copy the login's username to their clipboard. - - - Emeği Geçenler - Title for page that we use to give credit to resources that we use. - - - Sil - Delete an entity (verb). - - - Siliniyor... - Message shown when interacting with the server - - - Gerçekten silmek istiyor musunuz? Bu işlem geri alınamaz. - Confirmation alert message when deleteing something. - - - Düzenle - - - Klasörü Düzenle - - - E-posta - Short label for an email address. - - - E-posta Adresi - Full label for a email address. - - - Bize Yazın - - - Geribildirimde bulunmak ve yardım almak için bize mail atabilirsiniz. - - - PIN kodunuzu girin. - - - Favoriler - Title for your favorite items in the vault. - - - Bir Hata Raporlayın - - - Bizim GitHub depomuzda bir sorun bildirin. - - - Doğrulamak için parmak izinizi kullanın. - - - Klasör - Label for a folder. - - - Yeni klasör oluşturuldu. - - - Klasör silindi. - - - Klasör yok - Items that have no folder specified go in this special "catch-all" folder. - - - Klasörler - - - Klasör güncellendi. - - - Web sitesine git - The button text that allows user to launch the website to their web browser. - - - Yardım ve Geribildirim - - - Gizle - Hide a secret value that is currently shown (password). - - - Lütfen devam etmeden önce internete bağlanın. - Description message for the alert when internet connection is required to continue. - - - İnternet Bağlantısı Gerekiyor - Title for the alert when internet connection is required to continue. - - - Ana Parola Geçersiz. Tekrar deneyin. - - - Geçersiz PIN. Yeniden deneyin. - - - Başlat - The button text that allows user to launch the website to their web browser. - - - Giriş Yap - The login button text (verb). - - - Giriş - Title for login page. (noun) - - - Çıkış Yap - The log out button text (verb). - - - Çıkmak istediğinize emin misiniz? - - - Ana Parola - Label for a master password. - - - Daha fazla - Text to define that there are more options things to see. - - - Kasam - The title for the vault page. - - - Ad - Label for an entity name. - - - Hayır - - - Notlar - Label for notes. - - - Tamam - Acknowledgement. - - - Parola - Label for a password. - - - Kaydet - Button text for a save operation (verb). - - - Kaydediliyor... - Message shown when interacting with the server - - - Ayarlar - The title for the settings page. - - - Göster - Reveal a hidden value (password). - - - Hesap silindi. - Confirmation message after successfully deleting a login. - - - Gönder - - - Sync - The title for the sync page. - - - Teşekkürler - - - Araçlar - The title for the tools page. - - - URl - Label for a uri/url. - - - Kilidi açmak için parmak izinizi kullanın - - - Kullanıcı Adı - Label for a username. - - - {0} alanı gereklidir. - Validation message for when a form field is left blank and is required to be entered. - - - {0} kopyalandı. - Confirmation message after suceessfully copying a value to the clipboard. - - - Parmak izinizi doğrulayın - - - Ana Parolayı Doğrula - - - PIN Doğrula - - - Sürüm - - - Görüntüle - - - Web sitemizi ziyaret edin - - - Yardım almak, haberlere göz atmak, bize ulaşmak ve bitwarden'ı nasıl kullanacağınızı öğrenmek için web sitemizi ziyaret edin. - - - Websitesi - Label for a website. - - - Evet - - - Hesap - - - Yeni hesabınız oluşturuldu! Şimdi giriş yapabilirsiniz. - - - Bir hesap ekle - - - Uygulama uzantısı - - - bitwarden erişilebilirlik hizmetini, uygulamalarınızdaki ve internetteki oturumlarınızı otomatik olarak doldurmak için kullanın. - - - Otomatik doldurma servisi - - - Belirsiz Karakterler Kullanma - - - bitwarden uygulama uzantısı - - - Kasaya yeni hesaplar eklemenin en kolay yolu bitwarden uygulama uzantısını kullanmaktır. bitwarden uygulama uzantısı kullanımı hakkında "Araçlar" ekranına giderek daha fazla bilgi edinin. - - - Oturum formlarınızı otomatik doldurmak için Safari ve diğer uygulamalarda bitwarden kullanın. - - - bitwarden Otomatik doldurma Servisi - - - Giriş bilgilerinizi otomatik olarak doldurmak için bitwarden erişim hizmetini kullanın. - - - E-postayı Değiştir - - - E-posta adresinizi bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz? - - - Ana Parolayı Değiştir - - - Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz? - - - Kapat - - - Çok Yakında! - - - Devam - - - Kopyalandı! - - - Parola kopyalandı! - - - Kullanıcı adı kopyalandı! - - - Hesap Oluştur - - - Hesap oluşturuluyor... - Message shown when interacting with the server - - - Hesabı düzenle - - - Otomatik Eşitlemeyi Etkinleştir - - - Ana parola ipucunuzu almak istediğiniz e-posta adresinizi girin. - - - Uygulama eklentisini tekrar etkinleştir - - - Neredeyse bitti! - - - Uygulama eklentisini etkinleştir - - - Safari'de paylaş simgesini kullanarak bitwarden'ı bulun (ipucu: menünün en alt satırında sağa ilerleyin). - Safari is the name of apple's web browser - - - Parolalarınıza anında erişin! - - - Oturum açmak için hazırsınız! - - - Desteklenen Uygulamaları Gör - - - Artık oturum açma işlemlerinizi Safari, Chrome ve diğer desteklenen uygulamalardan kolayca yapabilirsiniz. - - - Safari ve Chrome'da, paylaş simgesini kullanarak bitwarden'ı bulun (ipucu: paylaş menüsünün alt satırında sağa ilerleyin). - - - Uzantıyı başlatmak için menüdeki bitwarden simgesine dokunun. - - - bitwarden'ı Safari ve diğer uygulamalarda etkinleştirmek için menünün alt satırında "daha fazla" simgesine dokunun. - - - Favori - - - Parmak izi - - - Parola Oluştur - - - Ana parola ipucununuzu alın - - - Hesapları İçe Aktar - - - bitwarden.com web kasasından topluca hesaplarınızı içe aktarabilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz? - - - Diğer parola yöneticisi uygulamalardan hesaplarınızı topluca ve hızlıca içe aktarın. - - - Son Eşitleme: - - - Uzunluk - - - Kilitle - - - 15 dakika - - - 1 saat - - - 1 dakika - - - 4 saat - - - Hemen - - - Kilit Seçenekleri - - - Oturum açılıyor... - Message shown when interacting with the server - - - Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun. - - - Yönet - - - Parola onayı doğru değil. - - - Ana parola, kasanıza ulaşmak için kullanacağınız paroladır. Ana parolanızı unutmamanız çok önemlidir. Ana parolanızı unutmanız durumunda parolanızı geri getirecek herhangi bir yol bulunmuyor. - - - Ana Parola İpucu (isteğe bağlı) - - - Bir ana parola ipucu, eğer unuttursanız parolanızı hatırlamanıza yardımcı olabilir. - - - Ana parola en az 8 karakter uzunluğunda olmalıdır. - - - En Az Rakam - Minimum numeric characters for password generator settings - - - En Az Özel Karakter - Minimum special characters for password generator settings - - - Daha Fazla Ayar - - - Uzantıyı kullanabilmeniz için ana bitwarden uygulamasında giriş yapmalısınız. - - - Asla - - - Yeni hesap oluşturuldu. - - - Kasanızda hiç favoriniz yok. - - - Kasanızda hiç hesabınız yok. - - - Bu site için kasanızda hiç hesap yok. Eklemek için dokunun. - - - Bu hesap için bir kullanıcı adı veya parola yapılandırılmamış. - - - Tamam, anladım! - Confirmation, like "Ok, I understand it" - - - Ayarların varsayılan değerleri, ana bitwarden uygulamasının parola üreticisi aracından ayarlanır. - - - Seçenekler - - - Diğer - - - Parola oluşturuldu. - - - Parola Oluşturucu - - - Parola İpucu - - - Size ana parolanızın ipucunu içeren bir e-posta gönderdik. - - - Mevcut parolanın üzerine kaydetmek istediğinize emin misiniz? - - - bitwarden push bildirimleri kullanarak kasanızı otomatik olarak eşitlenmiş olarak saklar. En iyi kullanıcı deneyimi için push bildirimlerini aşağıda belirecek uyarıda "Tamam"ı seçerek etkinleştirin. - Push notifications for apple products - - - Uygulamaya puan verin - - - Lütfen iyi bir yorum yazarak bize yardım edin! - - - Uygulama Mağazası oylamaları yeni bitwarden sürümü yayınlandıktan sonra sıfırlanır. Lütfen güzel incelemeler yazarak gelişmemize katkı sağlayın! - - - Tekrar Parola Oluştur - - - Ana Parolayı Tekrar Yazın - - - Kasada Ara - - - Güvenlik - - - Gelişim Sürecine Gözatın - - - Seç - - - Pin Ayarla - - - Uygulama kilidini açmak için 4 haneli bir PIN kodu girin. - - - Hesap Bilgisi - - - Hesap güncellendi. - - - Gönderiliyor... - Message shown when interacting with the server - - - Eşitleniyor... - Message shown when interacting with the server - - - Eşleme tamamlandı. - - - Eşitleme başarısız oldu. - - - Kasayı Şimdi Eşitle - - - Dokunma Kimliği - What Apple calls their fingerprint reader. - - - İki Aşamalı Giriş - - - İki adımlı oturum açma, oturum açma işleminizi bir güvenlik anahtarı, kimlik doğrulayıcı uygulama, SMS, telefon görüşmesi veya e-posta gibi başka bir cihazla doğrulamanızı isteyerek hesabınızı daha güvenli hale getirir. İki adımlı oturum açma işlemini bitwarden.com web kasası üzerinden etkinleştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz? - - - {0} ile kilidi aç - - - PIN kod ile kilidi aç - - - Onaylanıyor - Message shown when interacting with the server - - - Doğrulama Kodu - - - Hesabı Görüntüle - - - bitwarden Web Kasası - - - bitwarden web kasası ile herhangi bir internet tarayıcısından oturumlarınızı yönetin. - - - Kimlik doğrulama uygulamanızı kayıp mı ettiniz? - - - Hesaplar - Screen title - - - Uzantı etkinleştirildi! - - - Simgeler - - - Çeviriler - - - {0} için hesaplar - This is used for the autofill service. ex. "Logins for twitter.com" - - - {0} için kasanızda hiç hesap bulunmuyor. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - bitwarden oto-doldurma bildirmini gördüğünüzde otomatik doldurma servisini başlamak için bildirime dokunun. - - - Tap this notification to auto-fill a login from your vault. - - - Erişebilirlik Ayarlarını Aç - - - 1. Android erişilebilirlik ayarları ekranında, Servisler başlığı altında "bitwarden"a dokunun. - - - 2. Düğmeye değiştirin ve Tamam'a basarak onaylayın. - - - Devre Dışı - - - Etkin - - - Durum - - - Beta - - - Kasaya yeni hesaplar eklemenin en kolay yolu bitwarden Otomatik doldurma hizmetidir. bitwarden otmatik doldurma servisinin kullanımı hakkında "Araçlar" ekranına giderek daha fazla bilgi edinin. - - - Otomatik doldur - - - Do you want to auto-fill or view this login? - - - Are you sure you want to auto-fill this login? It is not a complete match for "{0}". - - - Eşleşen Hesaplar - - - Olası Eşleşen Hesaplar - - - Ara - - - You are searching for an auto-fill login for "{0}". - - - Kasanızı Paylaşın - - - Diğer kullanıcılar ile giriş bilgilerinizi güvenle paylaşmak için bir organizasyon oluşturun. - - - Parola alanına odaklanıldığında tara - - - Yalnızca parola alanını seçtiğinizde bir otomatik doldurma bildirimi sunar. Bu ayar batarya kullanımını azaltma konusunda faydalı olabilir. - - - Kalıcı Bildirim - - - Her zaman otomatik doldurmayı öner ve yalnızca otomatik doldurmaya teşebbüs edilirse tara. Bu ayar batarya kullanımını azaltma konusunda faydalı olabilir. - - - Her zaman tara - - - Doldurma alanları için ekranı her zaman tara ve parola alanları bulunursa sadece otomatik doldurma önerisi göster. Bu varsayılan ayardır. - - - "{0}" uygulaması açılamıyor. - Message shown when trying to launch an app that does not exist on the user's device. - - - Kimlik doğrulayıcı uygulama - For 2FA - - - Kimlik doğrulayıcı uygulamanızdaki 6 haneli doğrulama kodunu girin. - For 2FA - - - {0} adresine e-postalanan 6 haneli doğrulama kodunu girin. - For 2FA - - - Oturum Açma Kullanılamıyor - For 2FA whenever there are no available providers on this device. - - - Bu hesab için iki adımlı giriş etkinleştirilmiş, ancak, yapılandırılmış iki adımlı sağlayıcıların hiçbiri bu aygıtta desteklenmiyor. Lütfen desteklenen bir cihaz kullanın ve/veya cihazlar arasında daha iyi desteklenen ek sağlayıcılar ekleyin (bir kimlik doğrulayıcı uygulaması gibi). - - - Kurtarma kodu - For 2FA - - - Beni hatırla - Remember my two-step login - - - Doğrulama kodunu yeniden e-postala - For 2FA - - - İki adımlı oturum açma seçenekleri - - - Başka bir iki adımlı giriş yöntemini kullan - - - Doğrulama e-postası gönderilemedi. Yeniden deneyin. - For 2FA - - - Doğrulama e-postası gönderildi. - For 2FA - - - Devam etmek için YubiKey NEO anahtarınızı cihazınızın arkasına doğru tutun. - - - YubiKey NEO Güvenlik Anahtarı - "YubiKey" is the product name and should not be translated. - - - Yeni bir ek ekle - - - Ekler - - - Dosya indirilemiyor. - - - Cihazınız bu dosya türünü açamıyor. - - - İndiriliyor... - Message shown when downloading a file - - - Bu ekin boyutu {0}. Dosyayı cihazınıza indirmek istediğinizden emin misiniz? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Kimlik doğrulama anahtarı (TOTP) - - - Doğrulama kodu (TOTP) - Totp code label - - - Kimlik doğrulama anahtarı eklendi. - - - Kimlik doğrulayıcı anahtar okunamıyor. - - - Tarama otomatik olarak gerçekleşecek. - - - Kameranızı QR koda yöneltin. - - - QR kodu taratın - - - Kamera - - - Fotoğraflar - - - TOTP Kopyalandı! - - - TOTP Kopyala - - - Giriş bilgilerinizde bir kimlik doğrulama anahtarı varsa, giriş bilgilerini otomatik olarak doldurduğunuzda TOTP doğrulama kodu da otomatik olarak panonuza kopyalanır. - - - Otomatik TOTP Kopyalamayı Kapat - - - Bu özelliği kullanmak için premium üyelik gereklidir. - - - Dosya eklendi - - - Ek dosya silindi - - - Dosya seç - - - Dosya - - - Dosya seçilmedi - - - Ek dosya yok. - - - Dosya Kaynağı - - - Özellik mevcut değil - - - En büyük dosya boyutu 100MB. - - - Şifreleme anahtarınızı güncelleştirene kadar bu özelliği kullanamazsınız. - - - Daha fazla bilgi edinin - - - API Sunucu URL - - - Özel Çevre - - - Üst düzey kullanıcılar için. Her servisin taban URL'sini bağımsız olarak belirleyebilirsiniz. - - - Çevre URL'ler kaydedildi. - - - {0} doğru biçimlendirilmemiş. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Kimlik Sunucu URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Öz-barındıran Çevre - - - Kurum içi barındırılan bitwarden kurulumunuzun taban URL'sini belirtin. - - - Sunucu URL - - - Web Kasası Sunucu URL - - - Tap this notification to view logins from your vault. - - - Özel Alanlar - - - Numarayı Kopyala - - - Güvenlik Kodunu Kopyala - - - Numara - - - Güvenlik Kodu - - - Ne tür bir öğe eklemek istiyorsunuz? - - - Kart - - - Kimlik - - - Hesap - - - Güvenli Not - - - Adres 1 - - - Adres 2 - - - Adres 3 - - - Nisan - - - Ağustos - - - Marka - - - Kart Sahibinin Adı - - - Kent / Şehir - - - Şirket - - - Ülke - - - Aralık - - - Dr - - - Son Kullanma Ayı - - - Son Kullanma Yılı - - - Şubat - - - İlk Ad - - - Ocak - - - Temmuz - - - Haziran - - - Soyad - - - Ehliyet Numarası - - - Mart - - - Mayıs - - - İkinci Ad - - - Bay - - - Bayan - - - Bayan - - - Kasım - - - Ekim - - - Pasaport Numarası - - - Telefon - - - Eylül - - - Sosyal Güvenlik Numarası - - - Eyalet / İl - - - Başlık - - - Posta Kodu - - - Adres - - - Son Kullanma Tarihi - - - Site simgelerini devre dışı bırak - - - Web Sitesi Simgeleri, kasanızdaki her giriş bilgisinin yanında o siteyi tanımanıza yardımcı olacak bir görüntü verir. - - - Sunucu URL simgeleri - - - bitwarden ile otomatik doldur - - - Kasa kilitli - - - Kasama git - - - Koleksiyonlar - - - Bu koleksiyonda öğe yok. - - - Bu klasörde öğe yok. - - - Otomatik Doldur Erişilebilirlik Hizmeti - - - bitwarden otomatik doldurma hizmeti; oturum açma, kredi kartı bilgileri ve kimlik bilgilerini cihazınızdaki diğer uygulamalarda doldurmaya yardımcı olmak için Android Otomatik Doldurma Sistemini kullanır. - - - Giriş bilgilerinizi otomatik olarak doldurmak için bitwarden erişim hizmetini kullanın. - - - Otomatik Doldur Ayarlarını Aç - - - Face ID - What Apple calls their facial recognition reader. - - - Doğrulamak için Face ID kullan. - - - Kilidi açmak için Face ID kullan - - - Face ID Doğrula - - - Windows Hello ile Kilit Aç - - - Windows Hello ile Doğrula - - - Windows Hello - - - Android otomatik doldurmayı sizin yerinize yapamıyor. Android ayarları> Sistem> Dil ve giriş> Gelişmiş> Otomatik doldurma servisleri'nden otomatik doldurma ayarlarını el ile yönlendirebilirsiniz. - - - Özel Alan Adı - - - Boolean - - - Gizli - - - Metin - - - Yeni Özel Alan - - - Ne tür bir özel alan eklemek istiyor musunuz? - - - Kaldır - - - Yeni URL - - - URL {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Ana alanadı - - - Varsayılan - - - Tam - - - Sunucu alanadı - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Kurallı ifade - A programming term, also known as 'RegEx'. - - - İle başlar - - - Eşleşen URL Tespiti - - - Eşleşme Tespiti - URI match detection for auto-fill. - - - Evet, Kaydet - - - Otomatik doldur ve kaydet - - - Kuruluş - An entity of multiple related people (ex. a team or business organization). - - - Yubikey'inizi cihazınızın üzerine yakın tutun. - - - Tekrar Deneyin - - - Devam etmek için Yubikey NEO'nuzu cihazınız arkasına doğru tutun. - - - Erişilebilirlik hizmeti, uygulamalar standart otomatik doldurma işlemini desteklemediğinde kullanışlı olabilir. - - - Parola Güncellendi - ex. Date this password was updated - - - Güncellendi - ex. Date this item was updated - - - Otomatik Doldurma Etkinleştirildi! - - - Otomatik Doldurmayı kullanabilmeniz için ana Bitwarden uygulamasında giriş yapmalısınız. - - - Uygulamalara ve web sitelerine giriş yaparken, giriş bilgileriniz artık klavyenizden kolayca erişilebilir. - - - Kullanmayı düşünmüyorsanız, telefonunuzun "Ayarlar" bölümünden diğer Otomatik Doldurma uygulamalarını devre dışı bırakmanızı öneririz. - - - Parolalarınızı hızlıca otomatik doldurmak için kasanıza doğrudan klavyeden erişin. - - - Cihazınızda parolaları otomatik doldurmayı etkinleştirmek için şu talimatları izleyin: - - - 1. iOS "Ayarlar" açın - - - 2. "Parolalar & Hesaplar" dokunun - - - 3. "Parolaları Otomatik Doldur" dokunun - - - 4. Otomatik doldurmayı etkinleştirin - - - 5. "Bitwarden" seçin - - - Parola Otomatik Doldurma - - - Kasanıza yeni hesaplar eklemenin en kolay yolu Bitwarden Parola Otomatik Doldurma uzantısını kullanmaktır. "Araçlar" ekranına giderek Bitwarden Parola Otomatik Doldurma uzantısının kullanımı hakkında daha fazla bilgi alın. - - diff --git a/src/App/Resources/AppResources.uk.Designer.cs b/src/App/Resources/AppResources.uk.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.uk.resx b/src/App/Resources/AppResources.uk.resx deleted file mode 100644 index 1a2a68f5f..000000000 --- a/src/App/Resources/AppResources.uk.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Про програму - - - Додати - Add/create a new entity (verb). - - - Додати теку - - - Додати запис - The title for the add item page. - - - Сталася помилка. - Alert title when something goes wrong. - - - Назад - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Скасувати - Cancel an operation. - - - Копіювати - Copy some value to your clipboard. - - - Копіювати пароль - The button text that allows a user to copy the login's password to their clipboard. - - - Копіювати ім'я користувача - The button text that allows a user to copy the login's username to their clipboard. - - - Подяки - Title for page that we use to give credit to resources that we use. - - - Видалити - Delete an entity (verb). - - - Видалення... - Message shown when interacting with the server - - - Ви справді хочете видалити? Цю дію неможливо скасувати. - Confirmation alert message when deleteing something. - - - Змінити - - - Редагувати теку - - - Е-пошта - Short label for an email address. - - - Адреса е-пошти - Full label for a email address. - - - Напишіть нам - - - Напишіть нам, щоб отримати допомогу чи залишити відгук. - - - Введіть PIN-код. - - - Обране - Title for your favorite items in the vault. - - - Повідомити про помилку - - - Створіть звіт в нашому репозиторії на GitHub. - - - Використовуйте для перевірки свій відбиток пальця. - - - Тека - Label for a folder. - - - Нову теку створено. - - - Теку видалено. - - - Без теки - Items that have no folder specified go in this special "catch-all" folder. - - - Теки - - - Теку оновлено. - - - Перейти на веб-сайт - The button text that allows user to launch the website to their web browser. - - - Допомога і зворотній зв'язок - - - Приховати - Hide a secret value that is currently shown (password). - - - Для продовження, під'єднайтесь до інтернету. - Description message for the alert when internet connection is required to continue. - - - Необхідно з'єднатися з інтернетом - Title for the alert when internet connection is required to continue. - - - Недійсний головний пароль. Спробуйте знову. - - - Недійсний PIN. Спробуйте знову. - - - Запустити - The button text that allows user to launch the website to their web browser. - - - Вхід - The login button text (verb). - - - Вхід - Title for login page. (noun) - - - Вихід - The log out button text (verb). - - - Ви дійсно хочете вийти? - - - Головний пароль - Label for a master password. - - - Більше - Text to define that there are more options things to see. - - - Моє сховище - The title for the vault page. - - - Назва - Label for an entity name. - - - Ні - - - Нотатки - Label for notes. - - - Ok - Acknowledgement. - - - Пароль - Label for a password. - - - Зберегти - Button text for a save operation (verb). - - - Збереження... - Message shown when interacting with the server - - - Налаштування - The title for the settings page. - - - Показати - Reveal a hidden value (password). - - - Запис було видалено. - Confirmation message after successfully deleting a login. - - - Відправити - - - Синхронізація - The title for the sync page. - - - Дякуємо - - - Інструменти - The title for the tools page. - - - URI - Label for a uri/url. - - - Відбиток для розблокування - - - Ім'я користувача - Label for a username. - - - Поле {0} є обов'язковим. - Validation message for when a form field is left blank and is required to be entered. - - - {0} скопійовано. - Confirmation message after suceessfully copying a value to the clipboard. - - - Перевірка відбитку пальця - - - Перевірка головного пароля - - - Перевірка PIN - - - Версія - - - Переглянути - - - Відвідайте наш веб-сайт - - - Відвідайте наш веб-сайт, щоб прочитати новини, написати нам, або дізнатися більше про те, як ми використовуємо Bitwarden. - - - Веб-сайт - Label for a website. - - - Так - - - Обліковий запис - - - Ваш обліковий запис створений! Тепер ви можете увійти. - - - Додати запис - - - Розширення - - - Використовуйте службу автозаповнення Bitwarden, щоб автоматично вводити паролі в програмах та на веб-сайтах. - - - Служба автозаповнення - - - Уникати неоднозначних символів - - - Розширення Bitwarden - - - Найлегшим способом додавання нових записів у сховище є використання розширення Bitwarden. Дізнайтеся більше про використання розширення Bitwarden в меню "Інструменти". - - - Використовуйте Bitwarden в Safari та інших програмах для автоматичного заповнення паролів. - - - Служба автозаповнення Bitwarden - - - Використовуйте службу автозаповнення Bitwarden, щоб автоматично вводити паролі. - - - Змінити адресу е-пошти - - - Ви можете змінити адресу е-пошти в сховищі на bitwarden.com. Хочете перейти на веб-сайт зараз? - - - Змінити головний пароль - - - Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на веб-сайт зараз? - - - Закрити - - - Незабаром! - - - Продовжити - - - Скопійовано! - - - Пароль скопійовано! - - - Ім'я користувача скопійовано! - - - Створити обліковий запис - - - Створення облікового запису... - Message shown when interacting with the server - - - Зміна запису - - - Увімкніть автоматичну синхронізацію - - - Введіть свою адресу е-пошти, щоб отримати підказку для головного пароля. - - - Повторно увімкнути розширення - - - Майже готово! - - - Увімкнути розширення - - - В Safari знайдіть Bitwarden, використовуючи піктограму Поділитися (підказка: прокрутіть вправо в нижньому рядку меню). - Safari is the name of apple's web browser - - - Отримайте миттєвий доступ до своїх паролів! - - - Ви готові до входу! - - - Дивитися підтримувані програми - - - Тепер ви можете легко отримувати доступ до своїх записів з Safari, Chrome та інших підтримуваних програм. - - - В Safari та Chrome знайдіть Bitwarden, використовуючи піктограму Поділитися (підказка: прокрутіть вправо в нижньому рядку меню Поділитися). - - - Торкніться піктограми Bitwarden в меню для запуску розширення. - - - Щоб увімкнути Bitwarden в Safari та інших програмах, торкніться піктограми "Ще" в нижньому рядку меню. - - - Обране - - - Відбиток пальця - - - Генерувати пароль - - - Отримати підказку для головного пароля - - - Імпорт записів - - - Ви можете робити масовий імпорт записів зі сховища на bitwarden.com. Хочете перейти на веб-сайт зараз? - - - Швидкий імпорт ваших записів з інших програм керування паролями. - - - Остання синхронізація: - - - Довжина - - - Заблокувати - - - 15 хвилин - - - 1 година - - - 1 хвилина - - - 4 години - - - Негайно - - - Час блокування - - - Вхід... - Message shown when interacting with the server - - - Для доступу до сховища увійдіть в обліковий запис, або створіть новий. - - - Керування - - - Підтвердження пароля неправильне. - - - Головний пароль використовується для доступу до вашого сховища. Дуже важливо, щоб ви запам'ятали його. Якщо ви забудете головний пароль, його неможливо буде відновити. - - - Підказка для головного пароля (необов'язково) - - - Якщо ви забудете головний пароль, підказка може допомогти вам згадати його. - - - Довжина головного пароля повинна бути не менше 8 символів. - - - Мінімум цифр - Minimum numeric characters for password generator settings - - - Мінімум спеціальних символів - Minimum special characters for password generator settings - - - Додаткові налаштування - - - Ви повинні увійти в основну програму Bitwarden, щоб використовувати розширення. - - - Ніколи - - - Новий запис створено. - - - У вашому сховищі немає обраного. - - - У вашому сховищі немає записів. - - - У вашому сховищі немає записів для цього веб-сайту/програми. Торкніться, щоб додати. - - - Цей запис не має імені користувача або пароля. - - - Гаразд, зрозуміло! - Confirmation, like "Ok, I understand it" - - - Типові параметри задаються в генераторі паролів основної програми Bitwarden. - - - Додатково - - - Інше - - - Пароль згенеровано. - - - Генератор паролів - - - Підказка для пароля - - - Ми надіслали вам лист з підказкою для головного пароля. - - - Ви дійсно хочете перезаписати поточний пароль? - - - Bitwarden автоматично синхронізує ваше сховище за допомогою push-сповіщень. Для найкращої роботи оберіть "Дозволити" в наступному запиті про увімкнення push-сповіщень. - Push notifications for apple products - - - Оцініть програму - - - Будь ласка, подумайте про те, щоб допомогти нам хорошим відгуком! - - - Оцінки в App Store скидаються з кожною новою версією Bitwarden. Чи не бажаєте залишити про нас гарний відгук? - - - Генерувати новий - - - Введіть головний пароль ще раз - - - Пошук - - - Безпека - - - Процес розробки - - - Обрати - - - Встановлення PIN - - - Введіть 4-значний PIN-код для розблокування програми. - - - Інформація про запис - - - Запис оновлено. - - - Відправлення... - Message shown when interacting with the server - - - Синхронізація... - Message shown when interacting with the server - - - Синхронізація завершена. - - - Збій синхронізації. - - - Синхронізувати зараз - - - Touch ID - What Apple calls their fingerprint reader. - - - Двохетапна перевірка - - - Двохетапна перевірка робить ваш обліковий запис більш захищеним, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою коду безпеки, програми авторизації, SMS, телефонного виклику, або е-пошти. Ви можете увімкнути двохетапну перевірку в сховищі на bitwarden.com. Хочете перейти на веб-сайт зараз? - - - Розблокування з {0} - - - Розблокування з PIN-кодом - - - Перевірка - Message shown when interacting with the server - - - Код підтвердження - - - Перегляд запису - - - Веб-сховище Bitwarden - - - Керуйте своїми паролями з будь-якого браузера за допомогою веб-сховища Bitwarden. - - - Втратили доступ до програми перевірки? - - - Записи - Screen title - - - Розширення активовано! - - - Піктограми - - - Переклади - - - Записи для {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - У вашому сховищі немає записів для {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Коли ви бачите сповіщення автозаповнення Bitwarden, ви можете торкнутися його для запуску служби автозаповнення. - - - Торкніться цього сповіщення для автозаповнення пароля. - - - Відкрити налаштування доступності - - - 1. На екрані налаштувань доступності Android, оберіть службу "Bitwarden". - - - 2. Змініть положення перемикача й схваліть запит. - - - Вимкнено - - - Увімкнено - - - Статус - - - Бета - - - Найлегшим способом додавання нових записів у сховище є використання служби автозаповнення Bitwarden. Дізнайтеся більше про використання служби автозаповнення Bitwarden в меню "Інструменти". - - - Автозаповнення - - - Автоматичне заповнення чи перегляд запису? - - - Ви дійсно хочете використати цей запис для автоматичного заповнення? Він не повністю відповідає "{0}". - - - Відповідні записи - - - Можливі відповідні записи - - - Пошук - - - Ви шукаєте запис для "{0}". - - - Поділитися своїм сховищем - - - Створіть організацію, щоб безпечно ділитися записами з іншими користувачами. - - - Сканувати коли вибрано поле пароля - - - Сканувати екран на наявність полів і пропонувати автозаповнення при виборі поля пароля. Це може допомогти заощадити заряд батареї. - - - Постійне сповіщення - - - Завжди пропонувати автозаповнення й сканувати поля лише після спроби автозаповнення. Це може допомогти заощадити заряд батареї. - - - Завжди сканувати - - - Завжди сканувати екран на наявність полів і пропонувати автозаповнення при виявленні поля введення пароля. Це типове значення. - - - Не вдається відкрити програму "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Програма авторизації - For 2FA - - - Введіть 6-значних код підтвердження з програми авторизації. - For 2FA - - - Введіть 6-значний код підтвердження, надісланий на {0}. - For 2FA - - - Вхід недоступний - For 2FA whenever there are no available providers on this device. - - - Цей обліковий запис має увімкнену двохетапну перевірку, однак, жоден з підтримуваних провайдерів для цього пристрою не підтримується. Будь ласка, скористайтеся підтримуваним пристроєм та/або додайте інших провайдерів, які мають кращу підтримку різних пристроїв (наприклад, програму авторизації). - - - Код відновлення - For 2FA - - - Запам'ятати мене - Remember my two-step login - - - Надіслати код підтвердження ще раз - For 2FA - - - Налаштування двохетапної перевірки - - - Використати інший спосіб двохетапної перевірки - - - Не вдається надіслати лист для перевірки. Спробуйте знову. - For 2FA - - - Лист для підтвердження надіслано. - For 2FA - - - Для продовження прикладіть свій YubiKey NEO до задньої сторони пристрою, або вставте його в порт USB свого пристрою. - - - Ключ безпеки YubiKey - "YubiKey" is the product name and should not be translated. - - - Додати нове вкладення - - - Вкладення - - - Увімкнути для завантаження файлу. - - - Ваш пристрій не може відкрити цей тип файлу. - - - Завантаження... - Message shown when downloading a file - - - Це вкладення має розмір {0}. Ви дійсно хочете завантажити його на свій пристрій? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Ключ авторизації (TOTP) - - - Код підтвердження (TOTP) - Totp code label - - - Ключ авторизації додано. - - - Не вдається прочитати ключ авторизації. - - - Сканування відбудеться автоматично. - - - Наведіть свою камеру на QR-код. - - - Сканувати QR-код - - - Камера - - - Фотографії - - - TOTP скопійовано! - - - Копіювати TOTP - - - Якщо ваш запис має вкладений ключ авторизації, код підтвердження TOTP автоматично копіюється до буфера обміну щоразу при автозаповненні. - - - Вимкнути авто-копіювання TOTP - - - Для використання цієї функції необхідний преміум статус. - - - Вкладення додано - - - Вкладення видалено - - - Обрати файл - - - Файл - - - Файл не вибрано - - - Немає вкладень. - - - Джерело файлу - - - Функція недоступна - - - Максимальний розмір файлу 100 Мб. - - - Ви не можете використовувати цю функцію доки не оновите свій ключ шифрування. - - - Докладніше - - - URL-адреса API - - - Власне середовище - - - Для досвідчених користувачів. Ви можете вказати основну URL-адресу окремо для кожної служби. - - - URL-адреси середовища збережено. - - - {0} є неправильним форматом. - Validation error when something is not formatted correctly, such as a URL or email address. - - - URL-адреса сервера ідентифікації - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Середовище власного хостингу - - - Вкажіть основну URL-адресу вашого локально розміщеного встановлення Bitwarden. - - - URL-адреса сервера - - - URL-адреса сервера веб-сховища - - - Торкніться цього сповіщення для перегляду записів з вашого сховища. - - - Власні поля - - - Копіювати номер - - - Копіювати код безпеки - - - Номер - - - Код безпеки - - - Який тип запису ви хочете додати? - - - Картка - - - Особисті дані - - - Вхід - - - Захищена нотатка - - - Адреса 1 - - - Адреса 2 - - - Адреса 3 - - - Квітень - - - Серпень - - - Тип картки - - - Ім'я власника картки - - - Місто / Селище - - - Компанія - - - Країна - - - Грудень - - - Доктор - - - Місяць завершення - - - Рік завершення - - - Лютий - - - Ім’я - - - Січень - - - Липень - - - Червень - - - Прізвище - - - Номер ліцензії - - - Березень - - - Травень - - - По батькові - - - Містер - - - Місіс - - - Міс - - - Листопад - - - Жовтень - - - Номер паспорта - - - Телефон - - - Вересень - - - Номер соціального страхування - - - Штат / Область - - - Звернення - - - Поштовий індекс - - - Адреса - - - Термін дії - - - Вимкнути піктограми веб-сайтів - - - Впізнавані піктограми веб-сайтів додаються біля кожного запису вашого сховища. - - - URL-адреса сервера піктограм - - - Автозаповнення з Bitwarden - - - Сховище заблоковано - - - Перейти в моє сховище - - - Збірки - - - В цій збірці немає записів. - - - В цій теці немає записів. - - - Служба автозаповнення - - - Служба автозаповнення Bitwarden використовує Android Autofill Framework для заповнення паролів, кредитних карток та особистої інформації в інших програмах на вашому пристрої. - - - Використовуйте службу автозаповнення Bitwarden, щоб автоматично вводити паролі, кредитні картки та особисту інформацію в інших програмах. - - - Відкрити налаштування автозаповнення - - - Розпізнавання обличчя - What Apple calls their facial recognition reader. - - - Використовувати розпізнавання обличчя для підтвердження. - - - Розпізнавання обличчя для розблокування - - - Підтвердження розпізнавання обличчя - - - Розблокувати з Windows Hello - - - Підтвердити з Windows Hello - - - Windows Hello - - - Нам не вдалося автоматично відкрити меню налаштувань автозаповнення Android. Ви можете перейти до меню налаштувань автозаповнення вручну в Налаштування > Система > Мова і введення > Додатково > Служба автозаповнення. - - - Власна назва поля - - - Логічне значення - - - Приховано - - - Текст - - - Нове власне поле - - - Який тип власного поля ви хочете додати? - - - Вилучити - - - Новий URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Основний домен - - - Типово - - - Точно - - - Вузол - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Звичайний вираз - A programming term, also known as 'RegEx'. - - - Починається з - - - Виявлення збігів URI - - - Виявлення збігів - URI match detection for auto-fill. - - - Так, і зберегти - - - Заповнити і зберегти - - - Організація - An entity of multiple related people (ex. a team or business organization). - - - Утримуйте Yubikey біля верхньої частини пристрою. - - - Спробуйте ще раз - - - Щоб продовжити, утримуйте YubiKey NEO навпроти задньої частини пристрою. - - - Сервіс доступності може бути корисним для використання, коли програми не підтримують стандартний сервіс автозаповнення. - - - Пароль оновлено - ex. Date this password was updated - - - Оновлено - ex. Date this item was updated - - - Автозаповнення активовано! - - - Ви повинні увійти в основну програму Bitwarden, щоб використовувати автозаповнення. - - - Тепер ваші записи доступні безпосередньо з вашої клавіатури під час входу в програмах і на веб-сайтах. - - - Ми радимо вимкнути будь-які інші програми з функцією автозаповнення в Параметрах, якщо ви не плануєте ними користуватися. - - - Отримуйте доступ до свого сховища безпосередньо з клавіатури, щоб швидко вводити паролі. - - - Для увімкнення автозаповнення паролів на вашому пристрої, дотримуйтесь цих інструкцій: - - - 1. Відкрийте "Параметри" свого пристрою iOS - - - 2. Оберіть "Паролі й облікові записи" - - - 3. Торкніться до "Автозаповнення паролів" - - - 4. Увімкніть автозаповнення - - - 5. Оберіть "Bitwarden" - - - Автозаповнення паролів - - - Найлегшим способом додавання нових записів у сховище є використання розширення для автозаповнення Bitwarden. Дізнайтеся більше про використання розширення для автозаповнення Bitwarden в меню "Інструменти". - - diff --git a/src/App/Resources/AppResources.vi.Designer.cs b/src/App/Resources/AppResources.vi.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.vi.resx b/src/App/Resources/AppResources.vi.resx deleted file mode 100644 index a59d53459..000000000 --- a/src/App/Resources/AppResources.vi.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Thông tin - - - Thêm - Add/create a new entity (verb). - - - Thêm thư mục - - - Thêm Mục - The title for the add item page. - - - Đã xảy ra lỗi chưa xác định. - Alert title when something goes wrong. - - - Quay lại - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - Hủy bỏ - Cancel an operation. - - - Sao chép - Copy some value to your clipboard. - - - Sao chép mật khẩu - The button text that allows a user to copy the login's password to their clipboard. - - - Sao chép tên đăng nhập - The button text that allows a user to copy the login's username to their clipboard. - - - Đóng góp - Title for page that we use to give credit to resources that we use. - - - Xóa - Delete an entity (verb). - - - Đang xoá... - Message shown when interacting with the server - - - Bạn có chắc chắn muốn xóa nó không? Thao tác này không thể được khôi phục. - Confirmation alert message when deleteing something. - - - Sửa - - - Chỉnh sửa thư mục - - - Email - Short label for an email address. - - - Địa chỉ email - Full label for a email address. - - - Gửi email cho chúng tôi - - - Gửi email trực tiếp cho chúng tôi để nhận trợ giúp hoặc để lại phản hồi. - - - Nhập mã PIN của bạn. - - - Yêu thích - Title for your favorite items in the vault. - - - Gửi báo cáo lỗi - - - Tạo một vấn đề tại trang GitHub của chúng tôi. - - - Sử dụng dấu vân tay của bạn để tiếp tục. - - - Thư mục - Label for a folder. - - - Đã tạo thư mục mới. - - - Đã xóa thư mục. - - - Không được phân loại - Items that have no folder specified go in this special "catch-all" folder. - - - Thư mục - - - Đã cập nhật thư mục. - - - Đi đến trang web - The button text that allows user to launch the website to their web browser. - - - Trợ giúp & Phản hồi - - - Ẩn - Hide a secret value that is currently shown (password). - - - Hãy kết nối internet trước khi tiếp tục. - Description message for the alert when internet connection is required to continue. - - - Yêu cầu kết nối Internet - Title for the alert when internet connection is required to continue. - - - Mật khẩu chủ không đúng. Thử lại. - - - Mã PIN không đúng. Thử lại. - - - Khởi chạy - The button text that allows user to launch the website to their web browser. - - - Đăng Nhập - The login button text (verb). - - - Đăng nhập - Title for login page. (noun) - - - Đăng xuất - The log out button text (verb). - - - Bạn có chắc chắn muốn đăng xuất không? - - - Mật khẩu - Label for a master password. - - - Thêm - Text to define that there are more options things to see. - - - Kho của tôi - The title for the vault page. - - - Tên mục - Label for an entity name. - - - Không - - - Ghi chú - Label for notes. - - - Ok - Acknowledgement. - - - Mật khẩu - Label for a password. - - - Lưu - Button text for a save operation (verb). - - - Đang lưu... - Message shown when interacting with the server - - - Cài đặt - The title for the settings page. - - - Hiện - Reveal a hidden value (password). - - - Mục đã bị xoá. - Confirmation message after successfully deleting a login. - - - Gửi - - - Đồng bộ - The title for the sync page. - - - Cảm ơn bạn - - - Công cụ - The title for the tools page. - - - Đường dẫn - Label for a uri/url. - - - Sử dụng dấu vân tay để mở khóa - - - Tên người dùng - Label for a username. - - - Trường {0} là bắt buộc. - Validation message for when a form field is left blank and is required to be entered. - - - {0} đã được sao chép. - Confirmation message after suceessfully copying a value to the clipboard. - - - Xác thực vân tay - - - Nhập lại mật khẩu - - - Xác thực mã PIN - - - Phiên bản - - - Xem - - - Ghé thăm trang web của chúng tôi - - - Truy cập trang web của chúng tôi để nhận trợ giúp, tin tức, địa chỉ email liên lạc và tìm hiểu thêm về cách sử dụng Bitwarden. - - - Trang web - Label for a website. - - - - - - Tài khoản - - - Tài khoản của bạn đã được tạo. Bạn có thể đăng nhập bây giờ. - - - Thêm một mục - - - Phần mở rộng ứng dụng - - - Cấp quyền truy cập sử dụng để Bitwarden tự động điền thông tin đăng nhập của bạn vào các ứng dụng và trang web. - - - Tự động điền tiêu chuẩn - - - Tránh các ký tự không rõ ràng - - - Phần mở rộng Bitwarden - - - Cách dễ nhất để thêm đăng nhập mới vào kho của bạn là từ Phần mở rộng Bitwarden. Tìm hiểu thêm về cách sử dụng Phần mở rộng Bitwarden bằng cách điều hướng đến màn hình "Công cụ". - - - Sử dụng Bitwarden trong Safari và các ứng dụng khác để tự động điền thông tin đăng nhập của bạn. - - - Dịch vụ tự động điền Bitwarden - - - Cấp quyền truy cập sử dụng để Bitwarden tự động điền thông tin đăng nhập giúp bạn. - - - Thay đổi email - - - Bạn có thể thay đổi địa chỉ email trong trang web Bitwarden. Bạn có muốn truy cập bitwarden.com bây giờ? - - - Thay đổi mật khẩu - - - Bạn có thể thay đổi mật khẩu chủ trong trang web Bitwarden. Bạn có muốn truy cập bitwarden.com bây giờ? - - - Đóng - - - Sắp tới! - - - Tiếp tục - - - Đã sao chép! - - - Đã sao chép mật khẩu! - - - Đã sao chép tên người dùng! - - - Tạo Tài Khoản - - - Đang tạo tài khoản... - Message shown when interacting with the server - - - Chỉnh sửa mục - - - Bật tự động đồng bộ - - - Nhập địa chỉ email tài khoản của bạn để nhận gợi ý mật khẩu. - - - Bật lại phần mở rộng ứng dụng - - - Sắp xong! - - - Bật phần mở rộng ứng dụng - - - Trong Safari, tìm Bitwarden bằng cách sử dụng biểu tượng chia sẻ (gợi ý: di chuyển sang phải ở cuối cùng của trình đơn). - Safari is the name of apple's web browser - - - Truy cập ngay vào mật khẩu của bạn! - - - Bạn đã sẵn sàng để đăng nhập! - - - Xem các ứng dụng được hỗ trợ - - - Thông tin đăng nhập của bạn giờ đây có thể dễ dàng truy cập từ Safari, Chrome và các ứng dụng được hỗ trợ khác. - - - Trong Safari và Chrome, tìm Bitwarden bằng cách sử dụng biểu tượng chia sẻ (gợi ý: di chuyển sang phải ở cuối cùng của trình đơn chia sẻ). - - - Chạm vào biểu tượng Bitwarden trong trình đơn để khởi chạy phần mở rộng. - - - Để bật Bitwarden trong Safari và các ứng dụng khác, nhấn vào biểu tượng "thêm" ở hàng dưới cùng của trình đơn. - - - Yêu thích - - - Vân tay - - - Tạo mật khẩu - - - Nhận gợi ý mật khẩu Bitwarden - - - Nhập mục - - - Bạn có thể nhập mật khẩu hàng loạt trên trang chủ Bitwarden. Bạn có muốn truy cập bitwarden.com bây giờ? - - - Dễ dàng nhập toàn bộ mật khẩu của bạn từ các ứng dụng quản lý mật khẩu khác. - - - Lần đồng bộ gần nhất: - - - Độ dài - - - Khóa - - - 15 phút - - - 1 giờ - - - 1 phút - - - 4 giờ - - - Tức thì - - - Khóa lại sau - - - Đang đăng nhập... - Message shown when interacting with the server - - - Đăng nhập hoặc tạo tài khoản mới để truy cập kho mật khẩu của bạn. - - - Quản lý - - - Mật khẩu xác nhận không trùng khớp. - - - Mật khẩu chủ là mật khẩu bạn sử dụng để truy cập kho mật khẩu của bạn. Nó rất quan trọng nên bạn không được quên mật khẩu chủ của mình. Không thể khôi phục lại mật khẩu chủ nếu bạn quên nó. - - - Gợi ý mật khẩu chủ (tùy chọn) - - - Một gợi ý mật khẩu có thể giúp bạn nhớ lại mật khẩu chủ của bạn nếu bạn quên nó. - - - Mật khẩu chủ phải có ít nhất 8 kí tự. - - - Số chữ số tối thiểu - Minimum numeric characters for password generator settings - - - Số kí tự đặc biệt tối thiểu - Minimum special characters for password generator settings - - - Cài đặt Khác - - - Bạn phải đăng nhập vào ứng dụng Bitwarden trước khi sử dụng phần mở rộng. - - - Không bao giờ - - - Đã tạo mục mới. - - - Không có mục yêu thích nào trong kho của bạn. - - - Không có mục nào trong kho của bạn. - - - Không có mục nào trong kho của bạn phù hợp với trang web này. Nhấn để thêm. - - - Thông tin đăng nhập này không có tên người dùng hoặc mật khẩu được định cấu hình. - - - Ok, tôi đã hiểu! - Confirmation, like "Ok, I understand it" - - - Các tùy chọn mặc định được thiết lập trong trình tạo mật khẩu của ứng dụng Bitwarden. - - - Tùy chọn - - - Khác - - - Đã tạo mật khẩu. - - - Tạo mật khẩu - - - Gợi ý mật khẩu - - - Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chủ của bạn. - - - Bạn có chắc chắn muốn ghi đè mật khẩu hiện tại không? - - - Bitwarden giữ kho mật khẩu của bạn luôn được đồng bộ tự động hóa bằng cách sử dụng thông báo đẩy. Để mang lại trải nghiệm tốt nhất, hãy chọn "Đồng ý" trên bảng thông báo sau khi được yêu cầu bật thông báo đẩy. - Push notifications for apple products - - - Đánh giá ứng dụng - - - Xin hãy nhìn nhận và đánh giá tốt cho chúng tôi! - - - Xếp hạng trên App Store sẽ được đặt lại sau mỗi bản cập nhật của Bitwarden. Bạn có thể cảm ơn chúng tôi bằng cách đánh giá 5 sao cho Bitwarden! - - - Tạo lại mật khẩu - - - Nhập lại mật khẩu chủ - - - Tìm kiếm - - - Bảo mật - - - Xem lịch sử phát triển - - - Chọn - - - Thiết lập mã PIN - - - Nhập mã PIN 4 chữ số để mở khóa ứng dụng. - - - Thông tin mục - - - Đã cập nhật mục. - - - Đang gửi... - Message shown when interacting with the server - - - Đang đồng bộ... - Message shown when interacting with the server - - - Đồng bộ hoàn tất. - - - Đồng bộ thất bại. - - - Đồng bộ kho mật khẩu ngay - - - Touch ID - What Apple calls their fingerprint reader. - - - Xác thực hai lớp - - - Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh đăng nhập của bạn bằng khóa bảo mật, ứng dụng xác thực, tin nhắn, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong trang web Bitwarden. Bạn có muốn ghé thăm bitwarden.com bây giờ? - - - Mở khóa với {0} - - - Mở khóa với mã PIN - - - Đang xác nhận - Message shown when interacting with the server - - - Mã xác nhận - - - Xem mục - - - Trang web Bitwarden - - - Quản lý mật khẩu của bạn từ mọi nơi. - - - Mất ứng dụng xác thực? - - - Mục - Screen title - - - Đã kích hoạt Phần mở rộng! - - - Biểu tượng - - - Bản dịch - - - Mục cho {0} - This is used for the autofill service. ex. "Logins for twitter.com" - - - Không có mục nào trong kho của bạn thích hợp với {0}. - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - Khi bạn thấy thông báo tự động điền của Bitwarden, bạn có thể nhấn vào nó để chạy dịch vụ tự động điền. - - - Chạm vào thông báo này để tự động điền thông tin đăng nhập từ kho của bạn. - - - Mở cài đặt trợ năng - - - 1. Trên màn hình Cài đặt Trợ năng, chọn "Bitwarden" trong phần 'Các dịch vụ đã tải xuống'. - - - 2. Gạt công tắc và nhấn OK để chấp nhận. - - - Vô hiệu hóa - - - Kích hoạt - - - Trạng thái - - - Beta - - - Cách dễ nhất để thêm một đăng nhập mới vào kho của bạn là bằng dịch vụ Tự động điền của Bitwarden. Tìm hiểu thêm về cách sử dụng dịch vụ Tự động điền của Bitwarden trong trang 'Công cụ'. - - - Tự động điền - - - Bạn có muốn tự động điền hoặc xem đăng nhập này? - - - Bạn có chắc chắn muốn tự động điền vào mục này? Nó không hoàn toàn khớp với "{0}". - - - Khớp mục - - - Các mục có thể khớp - - - Tìm kiếm - - - Bạn đang tìm kiếm một tự động đăng nhập cho '{0}'. - - - Chia sẻ kho mật khẩu của bạn - - - Tạo một tổ chức để chia sẻ các thông tin đăng nhập của bạn một cách an toàn với những người dùng khác. - - - Quét khi bạn chọn trường Mật khẩu - - - Chỉ quét các trường trên màn hình và cung cấp thông báo tự động điền khi bạn chọn trường mật khẩu. Cài đặt này có thể giúp tiết kiệm pin. - - - Thông báo liên tục - - - Luôn cung cấp thông báo tự động điền và chỉ quét các trường sau khi thử tự động điền. Cài đặt này có thể giúp tiết kiệm pin. - - - Luôn luôn quét - - - Luôn quét các trường trên màn hình và chỉ cung cấp thông báo tự động điền nếu tìm thấy trường mật khẩu. Đây là thiết lập mặc định. - - - Không thể mở ứng dụng "{0}". - Message shown when trying to launch an app that does not exist on the user's device. - - - Ứng dụng Authenticator - For 2FA - - - Nhập mã xác nhận 6 chữ số từ ứng dụng xác thực của bạn. - For 2FA - - - Nhập mã xác nhận 6 chữ số đã được gửi tới email {0}. - For 2FA - - - Đăng nhập không sẵn có - For 2FA whenever there are no available providers on this device. - - - Tài khoản này đã kích hoạt xác thực hai lớp, tuy nhiên dịch vụ xác thực hai lớp đã chọn không được hỗ trợ trên thiết bị này. Vui lòng sử dụng thiết bị được hỗ trợ và/hoặc dịch vụ xác thực bổ sung có thể hoạt động tốt trên thiết bị (chẳng hạn như một ứng dụng xác thực). - - - Mã phục hồi - For 2FA - - - Ghi nhớ đăng nhập - Remember my two-step login - - - Gửi lại email chứa mã xác nhận - For 2FA - - - Tùy chọn xác thực hai lớp - - - Sử dụng phương pháp xác thực hai lớp khác - - - Không thể gửi email xác minh. Thử lại. - For 2FA - - - Đã gửi email xác minh. - For 2FA - - - Giữ YubiKey NEO lên phía sau của thiết bị để tiếp tục. - - - Khóa bảo mật YubiKey NEO - "YubiKey" is the product name and should not be translated. - - - Thêm tệp đính kèm mới - - - Tệp đính kèm - - - Không thể tải về tệp tin. - - - Thiết bị của bạn không thể mở loại tệp này. - - - Đang tải xuống... - Message shown when downloading a file - - - Tập tin đính kèm này có kích thước {0}. Bạn có chắc chắn muốn tải nó xuống thiết bị của mình? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - Khóa xác thực (TOTP) - - - Mã xác thực (TOTP) - Totp code label - - - Đã thêm khoá xác thực. - - - Không thể đọc khoá xác thực. - - - Quét sẽ thực hiện tự động. - - - Trỏ máy ảnh của bạn vào mã QR. - - - Quét mã QR - - - Máy ảnh - - - Ảnh - - - Đã sao chép TOTP! - - - Sao chép TOTP - - - Nếu đăng nhập của bạn có một khóa xác thực gắn liền với nó, mã xác nhận TOTP sẽ được tự động sao chép vào bộ nhớ tạm của bạn bất cứ khi nào bạn tự động điền thông tin đăng nhập. - - - Vô hiệu hoá tự động sao chép TOTP - - - Cần là thành viên cao cấp để sử dụng tính năng này. - - - Đã thêm tệp đính kèm - - - Đã xóa tệp đính kèm - - - Chọn tập tin - - - Tập tin - - - Chưa chọn tập tin - - - Không có tệp đính kèm. - - - Tệp nguồn - - - Tính năng không có sẵn - - - Kích thước tối đa của tệp tin là 100MB. - - - Bạn không thể sử dụng tính năng này cho đến khi bạn cập nhật khoá mã hóa. - - - Tìm Hiểu Thêm - - - Địa chỉ API máy chủ - - - Môi trường tùy chỉnh - - - Đối với người dùng nâng cao. Bạn có thể chỉ định liên kết cơ bản của mỗi dịch vụ một cách độc lập. - - - Địa chỉ môi trường đã được lưu. - - - {0} không đúng định dạng. - Validation error when something is not formatted correctly, such as a URL or email address. - - - Địa chỉ nhận dạng máy chủ - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - Môi trường độc lập - - - Chỉ định URL cơ sở on-premise của bạn để cài đặt máy chủ Bitwarden. - - - Địa chỉ máy chủ - - - URL máy chủ lưu trữ web - - - Chạm vào thông báo này để xem thông tin đăng nhập từ kho của bạn. - - - Trường tùy chỉnh - - - Chép số - - - Sao chép Mã bảo mật - - - Số - - - Mã bảo mật - - - Bạn muốn thêm loại mục nào? - - - Thẻ - - - Danh tính - - - Đăng nhập - - - Ghi chú bảo mật - - - Địa chỉ 1 - - - Địa chỉ 2 - - - Địa chỉ 3 - - - Tháng 4 - - - Tháng 8 - - - Thương hiệu - - - Tên chủ thẻ - - - Quận/Huyện/Thị trấn - - - Công ty - - - Quốc Gia - - - Tháng 12 - - - Tiến sĩ - - - Tháng Hết Hạn - - - Năm hết hạn - - - Tháng 2 - - - Tên - - - Tháng 1 - - - Tháng 7 - - - Tháng 6 - - - Họ - - - Số giấy phép - - - Tháng 3 - - - Tháng 5 - - - Tên đệm - - - Ông - - - - - - Chị - - - Tháng 11 - - - Tháng 10 - - - Số hộ chiếu - - - Số điện thoại - - - Tháng 9 - - - Số bảo hiểm xã hội - - - Tỉnh/Thành Phố - - - Tiêu đề - - - Mã bưu chính - - - Địa chỉ - - - Hết hạn - - - Vô hiệu hoá biểu tượng trang web - - - Biểu tượng trang web giúp bạn dễ dàng nhận dạng trang web trong kho mật khẩu của bạn. - - - Biểu tượng địa chỉ máy chủ - - - Tự động điền với Bitwarden - - - Kho đã khóa - - - Đến kho của tôi - - - Các Bộ Sưu Tập - - - Không có mục nào trong bộ sưu tập này. - - - Không có mục nào trong thư mục này. - - - Tự động điền bằng trợ năng - - - Dịch vụ tự động điền của Bitwarden sử dụng Bộ khung Tự động điền của Android để hỗ trợ điền thông tin đăng nhập, thẻ tín dụng và thông tin danh tính vào các ứng dụng khác trên thiết bị của bạn. - - - Sử dụng dịch vụ tự động điền của Bitwarden để điền thông tin đăng nhập, thẻ tín dụng và thông tin danh tính vào các ứng dụng khác. - - - Mở cài đặt Tự động điền - - - Face ID - What Apple calls their facial recognition reader. - - - Dùng Face ID để xác thực. - - - Dùng Face ID để mở khóa - - - Xác thực Face ID - - - Mở khóa với Windows Hello - - - Xác minh với Windows Hello - - - Windows Hello - - - Chúng tôi không thể mở cài đặt tự động điền cho bạn. Bạn có thể đi đến cài đặt tự động điền theo cách thủ công bằng cách vào Cài đặt > Hệ thống > Ngôn ngữ và nhập liệu > Nâng cao > Dịch vụ tự động điền. - - - Tên Trường Tùy Chỉnh - - - Đúng/Sai - - - Ẩn - - - Văn bản - - - Trường tùy chỉnh mới - - - Bạn muốn thêm loại trường tùy chỉnh nào? - - - Xoá - - - URL Mới - - - URL {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - Tên miền cơ sở - - - Mặc định - - - Chính xác - - - Máy chủ - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - Biểu thức chính quy - A programming term, also known as 'RegEx'. - - - Bắt đầu với - - - Độ khớp với URL - - - Độ khớp - URI match detection for auto-fill. - - - Được, lưu lại - - - Tự động điền và lưu - - - Tổ chức - An entity of multiple related people (ex. a team or business organization). - - - Đặt Yubikey của bạn ở phần đầu thiết bị. - - - Thử lại - - - Để tiếp tục, hãy đặt YubiKey NEO của bạn dựa vào mặt lưng của thiết bị. - - - Bạn có thể cần dùng dịch vụ trợ năng vì một số ứng dụng không hỗ trợ dịch vụ tự động điền của Android. - - - Password Updated - ex. Date this password was updated - - - Updated - ex. Date this item was updated - - - AutoFill Activated! - - - You must log into the main Bitwarden app before you can use AutoFill. - - - Your logins are now easily accessable right from your keyboard while logging into apps and websites. - - - We recommend disabling any other AutoFill apps under Settings if you do not plan to use them. - - - Access your vault directly from your keyboard to quickly autofill passwords. - - - To enable password autofill on your device, follow these instructions: - - - 1. Go to the iOS "Settings" app - - - 2. Tap "Passwords & Accounts" - - - 3. Tap "AutoFill Passwords" - - - 4. Turn on AutoFill - - - 5. Select "Bitwarden" - - - Password AutoFill - - - The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Tools" screen. - - diff --git a/src/App/Resources/AppResources.zh-Hans.Designer.cs b/src/App/Resources/AppResources.zh-Hans.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.zh-Hans.resx b/src/App/Resources/AppResources.zh-Hans.resx deleted file mode 100644 index 8d8d95493..000000000 --- a/src/App/Resources/AppResources.zh-Hans.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 关于 - - - 添加 - Add/create a new entity (verb). - - - 添加文件夹 - - - 添加项目 - The title for the add item page. - - - 发生错误。 - Alert title when something goes wrong. - - - 后退 - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - 取消 - Cancel an operation. - - - 复制 - Copy some value to your clipboard. - - - 复制密码 - The button text that allows a user to copy the login's password to their clipboard. - - - 复制用户名 - The button text that allows a user to copy the login's username to their clipboard. - - - 鸣谢 - Title for page that we use to give credit to resources that we use. - - - 删除 - Delete an entity (verb). - - - 正在删除... - Message shown when interacting with the server - - - 您真的要删除吗?这个操作无法撤消。 - Confirmation alert message when deleteing something. - - - 编辑 - - - 编辑文件夹 - - - 电子邮件 - Short label for an email address. - - - Email 地址 - Full label for a email address. - - - 给我们发邮件 - - - 请直接给我们发邮件来获得帮助,也可以留下反馈信息。 - - - 请输入您的 PIN 码。 - - - 收藏 - Title for your favorite items in the vault. - - - 发送错误报告 - - - 在我们的 GitHub 库汇报一个问题。 - - - 请验证指纹。 - - - 文件夹 - Label for a folder. - - - 新文件夹已创建。 - - - 文件夹已删除。 - - - 没有文件夹 - Items that have no folder specified go in this special "catch-all" folder. - - - 文件夹 - - - 文件夹已更新。 - - - 前往网站 - The button text that allows user to launch the website to their web browser. - - - 帮助和反馈 - - - 隐藏 - Hide a secret value that is currently shown (password). - - - 请链接因特网后再继续。 - Description message for the alert when internet connection is required to continue. - - - 需要因特网链接 - Title for the alert when internet connection is required to continue. - - - 无效的主密码,请重试。 - - - 无效的 PIN 码,请重试。 - - - 启动 - The button text that allows user to launch the website to their web browser. - - - 登录 - The login button text (verb). - - - 登录 - Title for login page. (noun) - - - 登出 - The log out button text (verb). - - - 您确定要登出吗? - - - 主密码 - Label for a master password. - - - 更多 - Text to define that there are more options things to see. - - - 我的密码库 - The title for the vault page. - - - 名称 - Label for an entity name. - - - - - - 笔记 - Label for notes. - - - - Acknowledgement. - - - 密码 - Label for a password. - - - 保存 - Button text for a save operation (verb). - - - 正在保存... - Message shown when interacting with the server - - - 设置 - The title for the settings page. - - - 显示 - Reveal a hidden value (password). - - - 项目已被删除。 - Confirmation message after successfully deleting a login. - - - 提交 - - - 同步 - The title for the sync page. - - - 非常感谢 - - - 工具 - The title for the tools page. - - - URI - Label for a uri/url. - - - 使用指纹解锁 - - - 用户名 - Label for a username. - - - 必须填写 {0} 。 - Validation message for when a form field is left blank and is required to be entered. - - - {0} 已复制。 - Confirmation message after suceessfully copying a value to the clipboard. - - - 验证指纹 - - - 验证主密码 - - - 验证 PIN 码 - - - 版本 - - - 查看 - - - 访问我们的网站 - - - 访问我们的网站来获得帮助、新闻,或者学习如何使用 Bitwarden。 - - - 网站 - Label for a website. - - - - - - 帐号 - - - 已经为您创建账号!您可以登录了。 - - - 添加项目 - - - App 扩展 - - - 使用 Bitwarden 无障碍服务在应用和网站自动填写您的登录项目。 - - - 自动填充 - - - 避免模棱两可 - - - Bitwarden App 扩展 - - - 最简单的添加网站的方法是 Bitwarden App 扩展。请到 “工具” 页面获得更多信息。 - - - 在 Safari 和其他应用中使用 Bitwarden 来自动填写登录信息。 - - - Bitwarden 自动填写服务 - - - 使用 Bitwarden 无障碍服务自动填写您的登录信息。 - - - 修改 Email - - - 您可以在 bitwarden.com 网页版密码库修改 email 地址。您现在要访问这个网站吗? - - - 修改主密码 - - - 您可以在 bitwarden.com 网页版密码库修改主密码。您现在要访问这个网站吗? - - - 关闭 - - - 即将提供! - - - 继续 - - - 已复制! - - - 已复制的密码! - - - 已复制的用户名! - - - 创建账号 - - - 正在创建账号... - Message shown when interacting with the server - - - 编辑项目 - - - 开启自动同步 - - - 请输入您帐号的 Email 地址来接收密码提示。 - - - 重新开启 App 扩展 - - - 就差一步! - - - 开启 App 扩展 - - - 在 Safari 中,使用共享图标寻找 Bitwarden (提示:在菜单最下面一行的右边)。 - Safari is the name of apple's web browser - - - 立即访问您的密码! - - - 您已经准备好登录了! - - - 受支持的应用 - - - 现在可以在 Safari、Chrome 以及其他支持的应用中方便的访问您的登录项目了。 - - - 在 Safari 和 Chrome 中,使用共享图标寻找 Bitwarden (提示: 在菜单最下面一行的右边). - - - 在菜单中点击 Bitwarden 图标来启动扩展。 - - - 要在 Safari 和其他应用打开 Bitwarden ,请在菜单最下面一行的最右边点击 "更多" 图标。 - - - 收藏 - - - 指纹 - - - 生成密码 - - - 获取主密码提示 - - - 导入项目 - - - 您可以在 bitwarden.com 网页版密码库批量导入登录项目。您现在要访问这个网站吗? - - - 快速从其他密码管理应用批量导入您的登录项目。 - - - 上次同步: - - - 长度 - - - 锁定 - - - 15 分钟 - - - 1 小时 - - - 1 分钟 - - - 4 小时 - - - 立即 - - - 锁定选项 - - - 正在登录... - Message shown when interacting with the server - - - 登录或者新建一个帐号来访问您的安全密码库。 - - - 管理 - - - 两次填写的密码不一致。 - - - 主密码是访问密码库的密码。非常重要,请不要忘记。忘记主密码以后没有途径可以恢复或重置。 - - - 主密码提示 (可选) - - - 主密码提示可以在你忘记密码时帮你回忆。 - - - 主密码至少需要 8 个字符。 - - - 数字 - Minimum numeric characters for password generator settings - - - 符号 - Minimum special characters for password generator settings - - - 更多设置 - - - 您必须先登录 Bitwarden 应用,才能使用扩展。 - - - 从不 - - - 已新建项目。 - - - 你没有收藏任何网站。 - - - 您的密码库里没有登录项目。 - - - 您的密码库里没有此网站的登录项目。请添加一个。 - - - 此网站没有配置过用户名或者密码。 - - - 好,开搞! - Confirmation, like "Ok, I understand it" - - - 选项默认值是由 Bitwarden 应用的密码生成工具设置。 - - - 选项 - - - 其他 - - - 密码已经生成。 - - - 密码生成器 - - - 密码提示 - - - 我们已经为您发送了包含主密码提示的邮件。 - - - 您确定要覆盖当前密码吗? - - - Bitwarden 使用推送通知来自动同步您的密码库。为了获得最佳体验,接下来询问是否开启通知时,请选择 “允许”。 - Push notifications for apple products - - - 为本应用打分 - - - 请给我们好评! - - - App Store 评分会在 Bitwarden 发布新版本时重置。请给我们好评! - - - 重新生成密码 - - - 重新输入主密码 - - - 搜索 - - - 安全 - - - 查看开发进度 - - - 选择 - - - 设置 PIN 码 - - - 输入一个 4 位的 PIN 码来解锁应用。 - - - 项目信息 - - - 项目已更新。 - - - 正在提交... - Message shown when interacting with the server - - - 正在同步... - Message shown when interacting with the server - - - 同步完成。 - - - 同步失败。 - - - 立即同步 - - - 触控 ID - What Apple calls their fingerprint reader. - - - 两步登录 - - - 两步登录需要您登录时输入一个从认证应用获得的安全码,这样能使您的账户更安全。两步登录可以在 bitwarden.com 网页版打开。您现在要访问这个网站吗? - - - 用 {0} 解锁 - - - 用PIN 码解锁 - - - 正在验证 - Message shown when interacting with the server - - - 验证码 - - - 查看项目 - - - Bitwarden 网页版密码库 - - - 使用 Bitwarden 网页版可以从任何浏览器管理您的登录项目。 - - - 认证应用已丢失? - - - 项目 - Screen title - - - 扩展激活 ! - - - 图标 - - - 译者 - - - {0} 的项目 - This is used for the autofill service. ex. "Logins for twitter.com" - - - 您的密码库里没有 {0} 的登录项目。 - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - 当你看到一个 Bitwarden 自动填写通知时,您可以用它来启动自动填写服务。 - - - 点击此通知,以便从您的密码库自动登录。 - - - 打开无障碍设置 - - - 1. 在 Android 的辅助功能设置屏幕上,触摸服务项下的 "Bitwarden" 的标题。 - - - 2. 切换开关,然后按确定接受。 - - - 禁用 - - - 启用 - - - 状态 - - - 测试版 - - - 向您的密码库中添加新登录项目的最简单方法,就是 Bitwarden 自动填写服务。转到“工具”屏幕了解更多有关使用 Bitwarden 自动填写服务的方法。 - - - 自动填充 - - - 你想要自动填充还是查看此登录项目? - - - 确定要自动登录?并不完全匹配 "{0}" 。 - - - 匹配项目 - - - 可能的匹配项 - - - 搜索 - - - 你正在为 "{0}" 寻找一个自动登录。 - - - 分享你的密码库 - - - 创建一个组织来安全地分享您的登录项目。 - - - 当焦点在密码字段时扫描 - - - 当你选择一个密码字段时,只扫描屏幕字段并提供自动填充通知。此设置可帮助延长电池寿命。 - - - 始终通知 - - - 始终提供自动填充通知,并且在试图自动填充后只扫描字段。此设置可帮助延长电池寿命。 - - - 始终扫描 - - - 始终扫描屏幕上的字段,如果找到密码字段,将只提供自动填充通知。这是默认设置。 - - - 无法打开应用 "{0}"。 - Message shown when trying to launch an app that does not exist on the user's device. - - - 身份验证 App - For 2FA - - - 请输入您的身份验证器 App 中的 6 位验证码。 - For 2FA - - - 请输入通过电子邮件发送给 {0} 的 6 位验证码。 - For 2FA - - - 登录不可用 - For 2FA whenever there are no available providers on this device. - - - 此帐户已启用的两步登录,但是,此设备不支持配置两步登录。请使用支持的设备,或者添加附加支持跨设备的提供程序(如验证器 App)。 - - - 恢复码 - For 2FA - - - 记住我 - Remember my two-step login - - - 再次发送验证码电子邮件 - For 2FA - - - 两步登录选项 - - - 使用另一种两步登录方法 - - - 无法发送验证电子邮件。请再试一次。 - For 2FA - - - 验证邮件已发送。 - For 2FA - - - 将您的 YubiKey NEO 靠在设备的后面,或者将 YubiKey 插入您设备的 USB 端口,按下按钮后继续。 - - - YubiKey 安全密钥 - "YubiKey" is the product name and should not be translated. - - - 添加新附件 - - - 附件 - - - 无法下载文件。 - - - 您的设备无法打开这种类型的文件。 - - - 正在下载…… - Message shown when downloading a file - - - 此附件大小是 {0} 。您确定要下载到设备? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - 验证器密钥 (TOTP) - - - 验证码 (TOTP) - Totp code label - - - 验证器密钥已添加。 - - - 无法读取验证器密钥。 - - - 将会自动扫描。 - - - 将相机对准 QR 码。 - - - 扫描二维码 - - - 相机 - - - 照片 - - - TOTP 已复制! - - - 复制 TOTP - - - 如果您的登录信息包含一个验证密钥,当自动填写该登录项目时,TOTP 验证码将会自动复制到剪贴板。 - - - 禁用自动 TOTP 备份 - - - 使用此功能需要高级会员资格。 - - - 附件已添加 - - - 附件已删除 - - - 选择文件 - - - 文件 - - - 未选定任何文件 - - - 沒有附件 - - - 文件源 - - - 功能不可用 - - - 文件最大为 100 MB。 - - - 在您更新加密密钥前,您不能使用此功能。 - - - 了解更多信息。 - - - API 服务器 URL - - - 自定义环境 - - - 适用于高级用户,你可以分别指定各个服务的基础 URL。 - - - 各环境 URL 已保存。 - - - {0} 的格式不正确。 - Validation error when something is not formatted correctly, such as a URL or email address. - - - 身份服务器 URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - 自托管环境 - - - 指定自己配置、托管的 Bitwarden 环境的基础 URL。 - - - 服务器 URL - - - Web 存储库服务器 URL - - - 点击此通知,以便从您的密码库查看登录项目。 - - - 自定义字段 - - - 复制号码 - - - 复制安全码 - - - 数字 - - - 安全码 - - - 要添加哪种类型的项目? - - - 卡片 - - - 身份 - - - 登录 - - - 安全笔记 - - - 地址 1 - - - 地址 2 - - - 地址 3 - - - 四月 - - - 八月 - - - 品牌 - - - 持卡人姓名 - - - 市 / 镇 - - - 公司 - - - 国家 - - - 十二月 - - - 博士 - - - 到期月份 - - - 到期年份 - - - 二月 - - - - - - 一月 - - - 七月 - - - 六月 - - - - - - 许可证编号 - - - 三月 - - - 五月 - - - 中间名 - - - 先生 - - - 夫人 - - - 女士 - - - 十一月 - - - 十月 - - - 护照号码 - - - 电话 - - - 九月 - - - 社会保险号码 - - - 州 / 省 - - - 称呼 - - - 邮编 / 邮政代码 - - - 地址 - - - 到期 - - - 禁用网站图标 - - - 在您密码库的每个登录项目旁边显示一个可识别的图标。 - - - 图标服务器 URL - - - 使用 Bitwarden 自动填写 - - - 密码库已锁定 - - - 转到我的密码库 - - - 集合 - - - 此集合中没有登录项目。 - - - 此文件夹中没有登录项目。 - - - 自动填充服务 - - - Bitwarden 自动填写服务使用 Android 自动填写框架来帮助您将登录、身份等信息填写到在的设备上的其他应用程序。 - - - 使用 Bitwarden 自动填写服务在应用和网站自动填写您的登录项目。 - - - 打开自动填充设置 - - - 面容 ID - What Apple calls their facial recognition reader. - - - 使用 "面容 ID" 进行验证。 - - - 使用 "面容 ID" 解锁 - - - 验证面容 ID - - - 使用 Windows Hello 解锁。 - - - 使用 Windows Hello 验证。 - - - Windows Hello - - - 我们无法为您自动打开 Android 自动填写设置菜单。您可以手动访问 Android 设置 > 系统 > 语言和输入 > 高级 > 自动填写服务 到达自动填写设置。 - - - 自定义字段名称 - - - 布尔 - - - 隐藏 - - - 文本 - - - 新建自定义字段 - - - 您想添加的自定义字段的类型是? - - - 移除 - - - 新 URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - 基础域 - - - 默认 - - - 精确 - - - 主机 - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - 正则表达式 - A programming term, also known as 'RegEx'. - - - 开始于 - - - URI 匹配检测 - - - 匹配检测 - URI match detection for auto-fill. - - - 是的,保存 - - - 自动填充并保存 - - - 组织 - An entity of multiple related people (ex. a team or business organization). - - - 把你的 Yubikey 靠近设备的顶部。 - - - 再试一次 - - - 要继续, 请将您的 YubiKey NEO 设备放在设备背面。 - - - 当应用程序不支持标准的自动填充服务时, 辅助功能服务可能会很有用。 - - - 密码更新于 - ex. Date this password was updated - - - 更新于 - ex. Date this item was updated - - - 自动填充已激活! - - - 您必须先登录 Bitwarden 应用,才可以使用自动填充。 - - - 现在您可以更轻松、快捷地登录应用和网站了! - - - 建议您在“设置”里关闭其它您不再使用的自动填充应用程序。 - - - 自动填充允许您直接从密码库选取需要登录的账户。 - - - 请按照以下步骤开启 Bitwarden 自动填充功能: - - - 1. 前往 iOS 的 “设置” 应用 - - - 2. 进入 “密码与帐户” - - - 3. 点击 “自动填充密码” - - - 4. 打开开关 - - - 5. 选择 Bitwarden 用来自动填充密码 - - - 自动填充密码 - - - Bitwarden 自动填充扩展是向密码库添加新帐户的最快途径,请前往 “工具” 页面查看详情。 - - diff --git a/src/App/Resources/AppResources.zh-Hant.Designer.cs b/src/App/Resources/AppResources.zh-Hant.Designer.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/App/Resources/AppResources.zh-Hant.resx b/src/App/Resources/AppResources.zh-Hant.resx deleted file mode 100644 index 469361cc5..000000000 --- a/src/App/Resources/AppResources.zh-Hant.resx +++ /dev/null @@ -1,1349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 關於 - - - 新增 - Add/create a new entity (verb). - - - 新增資料夾 - - - 新增項目 - The title for the add item page. - - - 發生錯誤。 - Alert title when something goes wrong. - - - 返回 - Navigate back to the previous screen. - - - Bitwarden - App name. Shouldn't ever change. - - - 取消 - Cancel an operation. - - - 複製 - Copy some value to your clipboard. - - - 複製密碼 - The button text that allows a user to copy the login's password to their clipboard. - - - 複製使用者名稱 - The button text that allows a user to copy the login's username to their clipboard. - - - 銘謝 - Title for page that we use to give credit to resources that we use. - - - 刪除 - Delete an entity (verb). - - - 正在刪除... - Message shown when interacting with the server - - - 確定要刪除嗎?刪除後將無法復原。 - Confirmation alert message when deleteing something. - - - 編輯 - - - 編輯資料夾 - - - 電子郵件 - Short label for an email address. - - - 電子郵件地址 - Full label for a email address. - - - 傳送電子郵件給我們 - - - 傳送電子郵件給我們以取得協助或提供意見反應。 - - - 請輸入 PIN 碼。 - - - 我的最愛 - Title for your favorite items in the vault. - - - Bug 回報 - - - 在我們的 GitHub 儲存庫中新增問題。 - - - 請驗證您的指紋。 - - - 資料夾 - Label for a folder. - - - 已新增資料夾。 - - - 已刪除資料夾。 - - - (未分類) - Items that have no folder specified go in this special "catch-all" folder. - - - 資料夾 - - - 已更新資料夾。 - - - 前往網站 - The button text that allows user to launch the website to their web browser. - - - 協助與意見反應 - - - 隱藏 - Hide a secret value that is currently shown (password). - - - 請先連線到網際網路後再繼續。 - Description message for the alert when internet connection is required to continue. - - - 需要連線網際網路 - Title for the alert when internet connection is required to continue. - - - 主密碼不正確,請重試。 - - - PIN 不正確,請重試。 - - - 開啟 - The button text that allows user to launch the website to their web browser. - - - 登入 - The login button text (verb). - - - 登入 - Title for login page. (noun) - - - 登出 - The log out button text (verb). - - - 您確定要登出嗎? - - - 主密碼 - Label for a master password. - - - 更多 - Text to define that there are more options things to see. - - - 我的密碼庫 - The title for the vault page. - - - 名稱 - Label for an entity name. - - - - - - 筆記 - Label for notes. - - - - Acknowledgement. - - - 密碼 - Label for a password. - - - 儲存 - Button text for a save operation (verb). - - - 正在儲存... - Message shown when interacting with the server - - - 設定 - The title for the settings page. - - - 顯示 - Reveal a hidden value (password). - - - 項目已被刪除。 - Confirmation message after successfully deleting a login. - - - 送出 - - - 同步 - The title for the sync page. - - - 非常感謝 - - - 工具 - The title for the tools page. - - - URI - Label for a uri/url. - - - 使用指紋解鎖 - - - 使用者名稱 - Label for a username. - - - 必須填入 {0} 。 - Validation message for when a form field is left blank and is required to be entered. - - - {0} 已複製。 - Confirmation message after suceessfully copying a value to the clipboard. - - - 驗證指紋 - - - 驗證主密碼 - - - 驗證 PIN 碼 - - - 版本 - - - 檢視 - - - 瀏覽我們的網站 - - - 瀏覽我們的網站以取得協助、了解新消息、傳送電子郵件給我們或瀏覽更多使用 Bitwarden 的秘訣。 - - - 網站 - Label for a website. - - - - - - 帳戶 - - - 帳戶已建立!現在可以登入了。 - - - 新增項目 - - - App 小工具 - - - 使用 Bitwarden 協助工具服務在應用程式和網站中自動填入您的登入資料。 - - - 自動填入 - - - 避免易混淆的字元 - - - Bitwarden App 小工具 - - - Bitwarden 小工具是新增登入資料最簡單的方法。請到 “工具” 頁面了解更多資訊。 - - - 在 Safari 和其他應用程式中使用 Bitwarden 自動填入登入資料。 - - - Bitwarden 自動填入服務 - - - 使用 Bitwarden 無障礙服務自動填入您的登入資料。 - - - 變更電子郵件 - - - 您可以在 bitwarden 網頁版變更電子郵件地址。現在前往? - - - 變更主密碼 - - - 您可以在 bitwarden 網頁版密碼庫變更主密碼。現在前往? - - - 關閉 - - - 即將推出! - - - 繼續 - - - 已複製! - - - 已複製密碼! - - - 已複製使用者名稱! - - - 新增帳戶 - - - 正在新增帳戶... - Message shown when interacting with the server - - - 編輯項目 - - - 開啟自動同步 - - - 請輸入您的帳户電子郵件地址以接收主密碼提示。 - - - 重新啟動 App 小工具 - - - 快要完成了! - - - 啟用 App 小工具 - - - 在 Safari 中的分享功能尋找 Bitwarden 圖示(提示:在最底行的最右邊)。 - Safari is the name of apple's web browser - - - 即時存取您的密碼! - - - 您已經準備好登入了! - - - 支援的程式 - - - 現在可以在 Safari、Chrome 以及其他支援的程式中方便地存取您的登入資料了。 - - - 在 Safari 或 Chrome 中的分享功能尋找 Bitwarden 圖示(提示:在最底行的最右邊)。 - - - 在選單中按一下 Bitwarden 圖示啟動小工具。 - - - 要在 Safari 或其他程式開啟 Bitwarden ,請在最底行的最右邊按 "更多" 圖示。 - - - 我的最愛 - - - 指紋 - - - 產生密碼 - - - 取得主密碼提示 - - - 匯入項目 - - - 您可以在 bitwarden 網頁版批次匯入登入資料。現在前往? - - - 快速從其他密碼管理應用程式中批次匯入您的登入資料。 - - - 上次同步於: - - - 長度 - - - 鎖定 - - - 15 分鐘 - - - 1 小時 - - - 1 分鐘 - - - 4 小時 - - - 立即 - - - 自動鎖定時間 - - - 正在登入... - Message shown when interacting with the server - - - 登入或新增帳戶來存取您的安全密碼庫。 - - - 管理 - - - 兩次填入的主密碼不符合。 - - - 主密碼是您存取密碼庫的密碼。請勿忘記主密碼,我們無法將您的主密碼復原或重設。 - - - 主密碼提示 (選用) - - - 主密碼提示可以在您忘記主密碼時幫助您回憶主密碼。 - - - 主密碼需要至少 8 個字元。 - - - 數字 - Minimum numeric characters for password generator settings - - - 符號 - Minimum special characters for password generator settings - - - 更多設定 - - - 您必須先登入 Bitwarden 才可使用小工具。 - - - 永不 - - - 已新增項目。 - - - 您沒有收藏任何網站。 - - - 您的密碼庫中沒有登入資料。 - - - 您的密碼庫中沒有此網站的登入資料。請新增一個。 - - - 此網頁沒有儲存過使用者名稱或密碼。 - - - 好的! - Confirmation, like "Ok, I understand it" - - - 選項預設值是由 Bitwarden 中的密碼產生器設定的。 - - - 進階選項 - - - 其他 - - - 密碼已產生。 - - - 密碼產生器 - - - 密碼提示 - - - 已寄出包含您主密碼提示的電子郵件。 - - - 您確定要覆寫目前的密碼嗎? - - - Bitwarden 使用推送通知來自動同步您的密碼庫。為了確保同步順暢,稍後系統將詢問您是否會開啟通知,請選擇“是”。 - Push notifications for apple products - - - 為 Bitwarden 評分 - - - 給我們留個好評吧! - - - App Store 評分會在 Bitwarden 每次更新時重設。給我們留個好評吧! - - - 重新產生密碼 - - - 重新輸入主密碼 - - - 搜尋 - - - 安全 - - - 瀏覽開發進度 - - - 選擇 - - - 設定 PIN 碼 - - - 輸入一個 4 位數 PIN 碼作為程式密碼。 - - - 項目資訊 - - - 項目已更新。 - - - 正在送出... - Message shown when interacting with the server - - - 正在同步... - Message shown when interacting with the server - - - 同步完成。 - - - 同步失敗。 - - - 立即同步 - - - Touch ID - What Apple calls their fingerprint reader. - - - 兩步驟登入 - - - 兩步驟登入需要您登入時輸入一個由驗證碼程式產生的安全碼,為您的帳戶增加一層保障。兩步驟登入可以在 bitwarden 網頁版啟用。現在前往? - - - 用 {0} 解鎖 - - - 用 PIN 碼解鎖 - - - 正在驗證 - Message shown when interacting with the server - - - 驗證碼 - - - 檢視項目 - - - Bitwarden 網頁版密碼庫 - - - 使用 Bitwarden 網頁版,從瀏覽器管理您的登入資料。 - - - 遺失了驗證碼程式? - - - 項目 - Screen title - - - 小工具已啟動! - - - 圖示 - - - 譯者 - - - {0} 的項目 - This is used for the autofill service. ex. "Logins for twitter.com" - - - 您的密碼庫中沒有 {0} 的登入資料。 - This is used for the autofill service. ex. "There are no items in your vault for twitter.com". - - - 當您看見 Bitwarden 的自動填入通知時,可以點選它以啟用自動填入服務。 - - - 按此自動填入來自密碼庫的登入資料。 - - - 開啟協助工具設定 - - - 1. 在 Android 的協助工具設定中,按一下服務中的 "Bitwarden" 。 - - - 2. 按一下開關,再按“確定”。 - - - 已停用 - - - 已啟用 - - - 狀態 - - - 測試版 - - - Bitwarden 自動填入服務是新增登入資料的最簡單方法。請到 “工具” 頁面了解更多使用 Bitwarden 自動填入服務的方法 。 - - - 自動填入 - - - 您想自動填入還是檢視登入資料? - - - 確定要自動填入此資料嗎?它和"{0}"不完全符合。 - - - 符合的項目 - - - 可能符合的項目 - - - 搜尋 - - - 您正在搜尋用於"{0}"的登入資料。 - - - 分享您的密碼庫 - - - 新增組織以安全地分享您的登入資料。 - - - 選取密碼欄位時掃描 - - - 當您選取了密碼欄位時才掃描螢幕和彈出自動輸入通知。這個設定可以減少電量消耗。 - - - 持續顯示通知 - - - 持續顯示自動輸入通知,選取了密碼欄位時才掃描螢幕。這個設定可以減少電量消耗。 - - - 持續掃描 - - - 持續掃描螢幕,偵測到密碼欄位時才彈出自動輸入通知。這是預設設定。 - - - 無法開啟應用程式"{0}"。 - Message shown when trying to launch an app that does not exist on the user's device. - - - 驗證器應用程式 - For 2FA - - - 輸入驗證器應用程式提供的 6 位數驗證碼。 - For 2FA - - - 輸入已傳送至電子郵件地址 {0} 的 6 位數驗證碼。 - For 2FA - - - 無法登入 - For 2FA whenever there are no available providers on this device. - - - 此帳戶已啟用兩步驟登入,但是本設備不支援已設定的兩步驟登入方式。請使用已支援的設備,及/或新增可以更好地跨設備的兩步驟登入方法(例如驗證器應用程式)。 - - - 復原碼 - For 2FA - - - 記住我的登入資訊 - Remember my two-step login - - - 再次傳送​​包含驗證碼的電子郵件 - For 2FA - - - 兩步驟登入選項 - - - 使用另一種兩步驟登入方法 - - - 無法傳送​​驗證電子郵件。再試一次。 - For 2FA - - - 已傳送驗證電子郵件 - For 2FA - - - 將您的 YubiKey NEO 緊貼於裝置後, 或者將 YubiKey 插入您裝置的 USB 連接埠, 按下按鈕後繼續。 - - - YubiKey 安全鑰匙 - "YubiKey" is the product name and should not be translated. - - - 新增附件 - - - 附件 - - - 無法下載檔案。 - - - 您的裝置無法開啟這種類型的檔案。 - - - 正在下載... - Message shown when downloading a file - - - 這個附件大小為 {0} 。確定要下載到您的裝置嗎? - The placeholder will show the file size of the attachment. Ex "25 MB" - - - 驗證器金鑰 (TOTP) - - - 驗證碼 (TOTP) - Totp code label - - - 驗證器金鑰已新增。 - - - 無法讀取驗證器金鑰。 - - - 掃描會自動執行。 - - - 將相機對準 QR code 。 - - - 掃描 QR Code - - - 相機 - - - 相片 - - - 複製了 TOTP ! - - - 複製 TOTP - - - 如果您的登入資料已有驗證器金鑰,TOTP 驗證碼會在您自動輸入時自動複製到您的剪貼簿。 - - - 禁止自動複製 TOTP - - - 進階會員才可使用此功能。 - - - 附件已新增 - - - 附件已移除 - - - 選擇檔案 - - - 檔案 - - - 沒有選擇檔案 - - - 沒有附件。 - - - 檔案來源 - - - 功能無法使用 - - - 最大檔案大小為 100MB。 - - - 更新加密金鑰前不能使用此功能。 - - - 了解更多 - - - API 伺服器 URL - - - 自訂環境 - - - 適用於進階使用者。您可以單獨指定各個服務的基礎 URL。 - - - 環境 URL 已儲存。 - - - {0} 的格式不正確。 - Validation error when something is not formatted correctly, such as a URL or email address. - - - 身分伺服器 URL - "Identity" refers to an identity server. See more context here https://en.wikipedia.org/wiki/Identity_management - - - 自我託管環境 - - - 指定自己設定、託管的 bitwarden 環境之基礎 URL。 - - - 伺服器 URL - - - 網頁版密碼庫伺服器 URL - - - 按此自動填入來自密碼庫的登入資料。 - - - 自訂欄位 - - - 複製號碼 - - - 複製安全代碼 - - - 號碼 - - - 安全代碼 - - - 您要新增哪種類型的項目? - - - 信用卡 - - - 身分 - - - 登入 - - - 安全筆記 - - - 地址 1 - - - 地址 2 - - - 地址 3 - - - 四月 - - - 八月 - - - 品牌 - - - 持卡人姓名 - - - 城市/城鎮 - - - 公司 - - - 國家 - - - 十二月 - - - Dr - - - 到期月份 - - - 到期年份 - - - 二月 - - - - - - 一月 - - - 七月 - - - 六月 - - - - - - 許可證編號 - - - 三月 - - - 五月 - - - 中間名 - - - Mr - - - Mrs - - - Ms - - - 十一月 - - - 十月 - - - 護照號碼 - - - 電話號碼 - - - 九月 - - - 社會保險號碼 - - - 州/省 - - - 稱呼 - - - 郵遞區號 - - - 地址 - - - 到期 - - - 停用網站圖示顯示功能 - - - 在您密碼庫的每個登入資料旁顯示一個可辨識圖示。 - - - 圖示伺服器 URL - - - 使用 Bitwarden 自動填入 - - - 密碼庫已鎖定 - - - 前往我的密碼庫 - - - 收藏 - - - 此收藏中沒有登入資料。 - - - 此資料夾中沒有登入資料。 - - - 自動填入服務 - - - Bitwarden 自動填入服務使用 Android 自動填入框架來幫助您將登入資料、信用卡和身分資訊填入至裝置上的其他應用程式中。 - - - 使用 Bitwarden 自動填入服務 ,幫您在網頁和應用程式中自動填入登入資料。 - - - 開啟自動填入設定 - - - Face ID - What Apple calls their facial recognition reader. - - - 使用 Face ID 進行驗證 - - - 使用 Face ID 解鎖 - - - 驗證 Face ID - - - 使用 Windows Hello 解鎖 - - - 使用 Windows Hello 驗證 - - - Windows Hello - - - 我們無法為您自動開啟 Android 自動填入設定選單。您可以手動造訪 Android 設定 > 系統 > 語言與輸入設定 > 進階 > 自動填入服務 到達自動填入設定。 - - - 自訂欄位名稱 - - - 布林值 - - - 隱藏 - - - 文字 - - - 新增自訂欄位 - - - 您要新增哪種類型的自訂欄位? - - - 移除 - - - 新增 URI - - - URI {0} - Label for a uri/url with position. i.e. URI 1, URI 2, etc - - - 基底網域 - - - 預設 - - - 完全符合 - - - 主機 - A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'. - - - 規則運算式 - A programming term, also known as 'RegEx'. - - - 開始於 - - - URI 相符偵測 - - - 相符偵測 - URI match detection for auto-fill. - - - 是,我要儲存 - - - 自動填入並儲存 - - - 組織 - An entity of multiple related people (ex. a team or business organization). - - - 將您的 Yubikey 靠近裝置的頂端。 - - - 再試一次 - - - 要繼續的話,請將您的 YubiKey NEO 靠在裝置的背面。 - - - 當應用程式不支援標準的自動填入服務時, 協助工具服務或許能夠幫助使用。 - - - 密碼已更新 - ex. Date this password was updated - - - 已更新 - ex. Date this item was updated - - - 自動填入已啟用! - - - 您必須先登入 Bitwarden 應用程式,才可以使用自動填入功能。 - - - 登人應用程式或網站時,您可以輕鬆地從鍵盤存取登入資料。 - - - 建議您在“設定”中關閉您不再使用的其他自動填入應用程式。 - - - 您可以直接從鍵盤存取密碼庫以快速的自動填入密碼。 - - - 請按照以下步驟開啟 Bitwarden 自動填入功能: - - - 1. 前往 iOS 的“設定”應用程式 - - - 2. 點選 "密碼和帳戶" - - - 3. 點選 "自動填入密碼" - - - 4. 開啟自動填入功能 - - - 5. 選擇 "Bitwarden" - - - 自動填入密碼 - - - Bitwarden 小工具是新增登入資料最簡單的方法。請到 “工具” 頁面了解更多資訊。 - - diff --git a/src/App/Services/AppIdService.cs b/src/App/Services/AppIdService.cs deleted file mode 100644 index d648437db..000000000 --- a/src/App/Services/AppIdService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Bit.App.Abstractions; - -namespace Bit.App.Services -{ - public class AppIdService : IAppIdService - { - private const string AppIdKey = "appId"; - private const string AnonymousAppIdKey = "anonymousAppId"; - private readonly ISecureStorageService _secureStorageService; - private Guid? _appId; - private Guid? _anonymousAppId; - - public AppIdService(ISecureStorageService secureStorageService) - { - _secureStorageService = secureStorageService; - } - - public string AppId => GetAppId(AppIdKey, ref _appId); - public string AnonymousAppId => GetAppId(AnonymousAppIdKey, ref _anonymousAppId); - - private string GetAppId(string key, ref Guid? appId) - { - if(appId.HasValue) - { - return appId.Value.ToString(); - } - - var appIdBytes = _secureStorageService.Retrieve(key); - if(appIdBytes != null) - { - appId = new Guid(appIdBytes); - return appId.Value.ToString(); - } - - appId = Guid.NewGuid(); - _secureStorageService.Store(key, appId.Value.ToByteArray()); - return appId.Value.ToString(); - } - } -} diff --git a/src/App/Services/AppSettingsService.cs b/src/App/Services/AppSettingsService.cs deleted file mode 100644 index 3d609d225..000000000 --- a/src/App/Services/AppSettingsService.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Plugin.Settings.Abstractions; - -namespace Bit.App.Services -{ - public class AppSettingsService : IAppSettingsService - { - private readonly ISettings _settings; - - public AppSettingsService( - ISettings settings) - { - _settings = settings; - } - - public bool Locked - { - get - { - return _settings.GetValueOrDefault(Constants.Locked, false); - } - set - { - _settings.AddOrUpdateValue(Constants.Locked, value); - } - } - - public int FailedPinAttempts - { - get - { - return _settings.GetValueOrDefault(Constants.FailedPinAttempts, 0); - } - set - { - _settings.AddOrUpdateValue(Constants.FailedPinAttempts, value); - } - } - - public DateTime LastActivity - { - get - { - return _settings.GetValueOrDefault(Constants.LastActivityDate, DateTime.MinValue); - } - set - { - _settings.AddOrUpdateValue(Constants.LastActivityDate, value); - } - } - - public DateTime LastCacheClear - { - get - { - return _settings.GetValueOrDefault(Constants.LastCacheClearDate, DateTime.MinValue); - } - set - { - _settings.AddOrUpdateValue(Constants.LastCacheClearDate, value); - } - } - - public bool AutofillPersistNotification - { - get - { - return _settings.GetValueOrDefault(Constants.AutofillPersistNotification, false); - } - set - { - _settings.AddOrUpdateValue(Constants.AutofillPersistNotification, value); - } - } - - public bool AutofillPasswordField - { - get - { - return _settings.GetValueOrDefault(Constants.AutofillPasswordField, false); - } - set - { - _settings.AddOrUpdateValue(Constants.AutofillPasswordField, value); - } - } - - public bool DisableWebsiteIcons - { - get - { - return _settings.GetValueOrDefault(Constants.SettingDisableWebsiteIcons, false); - } - set - { - _settings.AddOrUpdateValue(Constants.SettingDisableWebsiteIcons, value); - } - } - - public string SecurityStamp - { - get - { - return _settings.GetValueOrDefault(Constants.SecurityStamp, null); - } - set - { - _settings.AddOrUpdateValue(Constants.SecurityStamp, value); - } - } - - public string BaseUrl - { - get - { - return _settings.GetValueOrDefault(Constants.BaseUrl, null); - } - set - { - if(value == null) - { - _settings.Remove(Constants.BaseUrl); - return; - } - - _settings.AddOrUpdateValue(Constants.BaseUrl, value); - } - } - - public string WebVaultUrl - { - get - { - return _settings.GetValueOrDefault(Constants.WebVaultUrl, null); - } - set - { - if(value == null) - { - _settings.Remove(Constants.WebVaultUrl); - return; - } - - _settings.AddOrUpdateValue(Constants.WebVaultUrl, value); - } - } - - public string ApiUrl - { - get - { - return _settings.GetValueOrDefault(Constants.ApiUrl, null); - } - set - { - if(value == null) - { - _settings.Remove(Constants.ApiUrl); - return; - } - - _settings.AddOrUpdateValue(Constants.ApiUrl, value); - } - } - - public string IdentityUrl - { - get - { - return _settings.GetValueOrDefault(Constants.IdentityUrl, null); - } - set - { - if(value == null) - { - _settings.Remove(Constants.IdentityUrl); - return; - } - - _settings.AddOrUpdateValue(Constants.IdentityUrl, value); - } - } - - public string IconsUrl - { - get => _settings.GetValueOrDefault(Constants.IconsUrl, null); - set - { - if(value == null) - { - _settings.Remove(Constants.IconsUrl); - return; - } - - _settings.AddOrUpdateValue(Constants.IconsUrl, value); - } - } - - public bool ClearCiphersCache - { - get - { - return _settings.GetValueOrDefault(Constants.ClearCiphersCache, false); - } - set - { - _settings.AddOrUpdateValue(Constants.ClearCiphersCache, value); - } - } - - public bool ClearExtensionCiphersCache - { - get - { - return _settings.GetValueOrDefault(Constants.ClearExtensionCiphersCache, false); - } - set - { - _settings.AddOrUpdateValue(Constants.ClearExtensionCiphersCache, value); - } - } - - public bool OrganizationGivesPremium - { - get - { - return _settings.GetValueOrDefault(Constants.OrgGivesPremium, false); - } - set - { - _settings.AddOrUpdateValue(Constants.OrgGivesPremium, value); - } - } - } -} diff --git a/src/App/Services/AuthService.cs b/src/App/Services/AuthService.cs deleted file mode 100644 index 8094b0814..000000000 --- a/src/App/Services/AuthService.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Api; -using Plugin.Settings.Abstractions; -using Bit.App.Models; -using System.Linq; -using Bit.App.Enums; -using Xamarin.Forms; -using Bit.App.Pages; -using Bit.App.Controls; -using XLabs.Ioc; - -namespace Bit.App.Services -{ - public class AuthService : IAuthService - { - private const string EmailKey = "email"; - private const string KdfKey = "kdf"; - private const string KdfIterationsKey = "kdfIterations"; - private const string UserIdKey = "userId"; - private const string PreviousUserIdKey = "previousUserId"; - private const string PinKey = "pin"; - - private readonly ISecureStorageService _secureStorage; - private readonly ITokenService _tokenService; - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettingsService; - private readonly ICryptoService _cryptoService; - private readonly IConnectApiRepository _connectApiRepository; - private readonly IAccountsApiRepository _accountsApiRepository; - private readonly IAppIdService _appIdService; - private readonly IDeviceInfoService _deviceInfoService; - private readonly IDeviceApiRepository _deviceApiRepository; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - - private string _email; - private KdfType? _kdf; - private int? _kdfIterations; - private string _userId; - private string _previousUserId; - private string _pin; - - public AuthService( - ISecureStorageService secureStorage, - ITokenService tokenService, - ISettings settings, - IAppSettingsService appSettingsService, - ICryptoService cryptoService, - IConnectApiRepository connectApiRepository, - IAccountsApiRepository accountsApiRepository, - IAppIdService appIdService, - IDeviceInfoService deviceInfoService, - IDeviceApiRepository deviceApiRepository, - IGoogleAnalyticsService googleAnalyticsService) - { - _secureStorage = secureStorage; - _tokenService = tokenService; - _settings = settings; - _appSettingsService = appSettingsService; - _cryptoService = cryptoService; - _connectApiRepository = connectApiRepository; - _accountsApiRepository = accountsApiRepository; - _appIdService = appIdService; - _deviceInfoService = deviceInfoService; - _deviceApiRepository = deviceApiRepository; - _googleAnalyticsService = googleAnalyticsService; - } - - public string UserId - { - get - { - if(!string.IsNullOrWhiteSpace(_userId)) - { - return _userId; - } - - var userId = _settings.GetValueOrDefault(UserIdKey, string.Empty); - if(!string.IsNullOrWhiteSpace(userId)) - { - _userId = userId; - } - - return _userId; - } - set - { - if(value != null) - { - _settings.AddOrUpdateValue(UserIdKey, value); - } - else - { - PreviousUserId = _userId; - _settings.Remove(UserIdKey); - } - - _userId = value; - } - } - - public string PreviousUserId - { - get - { - if(!string.IsNullOrWhiteSpace(_previousUserId)) - { - return _previousUserId; - } - - var previousUserId = _settings.GetValueOrDefault(PreviousUserIdKey, string.Empty); - if(!string.IsNullOrWhiteSpace(previousUserId)) - { - _previousUserId = previousUserId; - } - - return _previousUserId; - } - private set - { - if(value != null) - { - _settings.AddOrUpdateValue(PreviousUserIdKey, value); - _previousUserId = value; - } - } - } - - public bool UserIdChanged => PreviousUserId != UserId; - - public string Email - { - get - { - if(!string.IsNullOrWhiteSpace(_email)) - { - return _email; - } - - var email = _settings.GetValueOrDefault(EmailKey, string.Empty); - if(!string.IsNullOrWhiteSpace(email)) - { - _email = email; - } - - return _email; - } - set - { - if(value != null) - { - _settings.AddOrUpdateValue(EmailKey, value); - } - else - { - _settings.Remove(EmailKey); - } - - _email = value; - } - } - - public KdfType Kdf - { - get - { - if(!_kdf.HasValue) - { - _kdf = (KdfType)_settings.GetValueOrDefault(KdfKey, (short)KdfType.PBKDF2_SHA256); - } - return _kdf.Value; - } - set - { - _settings.AddOrUpdateValue(KdfKey, (short)value); - _kdf = value; - } - } - - public int KdfIterations - { - get - { - if(!_kdfIterations.HasValue) - { - _kdfIterations = _settings.GetValueOrDefault(KdfIterationsKey, 5000); - } - return _kdfIterations.Value; - } - set - { - _settings.AddOrUpdateValue(KdfIterationsKey, value); - _kdfIterations = value; - } - } - - public bool IsAuthenticated - { - get - { - return _cryptoService.Key != null && - !string.IsNullOrWhiteSpace(_tokenService.Token) && - !string.IsNullOrWhiteSpace(UserId); - } - } - - public string PIN - { - get - { - if(_pin != null) - { - return _pin; - } - - var pinBytes = _secureStorage.Retrieve(PinKey); - if(pinBytes == null) - { - return null; - } - - _pin = Encoding.UTF8.GetString(pinBytes, 0, pinBytes.Length); - return _pin; - } - set - { - if(value != null) - { - var pinBytes = Encoding.UTF8.GetBytes(value); - _secureStorage.Store(PinKey, pinBytes); - } - else - { - _secureStorage.Delete(PinKey); - } - - _pin = value; - } - } - - public bool BelongsToOrganization(string orgId) - { - return !string.IsNullOrWhiteSpace(orgId) && (_cryptoService.OrgKeys?.ContainsKey(orgId) ?? false); - } - - public void LogOut(string logoutMessage = null) - { - CipherService.CachedCiphers = null; - _tokenService.Token = null; - UserId = null; - Email = null; - _cryptoService.ClearKeys(); - _settings.Remove(Constants.SecurityStamp); - _settings.Remove(Constants.PushLastRegistrationDate); - _settings.Remove(Constants.Locked); - - Task.Run(async () => await _deviceApiRepository.PutClearTokenAsync(_appIdService.AppId)); - - _googleAnalyticsService.TrackAppEvent("LoggedOut"); - - MessagingCenter.Send(Application.Current, "LoggedOut"); - Device.BeginInvokeOnMainThread(() => Application.Current.MainPage = new ExtendedNavigationPage(new HomePage())); - if(!string.IsNullOrWhiteSpace(logoutMessage)) - { - Resolver.Resolve()?.Toast(logoutMessage); - } - } - - public async Task TokenPostAsync(string email, string masterPassword) - { - Kdf = KdfType.PBKDF2_SHA256; - KdfIterations = 5000; - var preloginResponse = await _accountsApiRepository.PostPreloginAsync( - new PreloginRequest { Email = email }); - if(preloginResponse.Succeeded) - { - Kdf = preloginResponse.Result.Kdf; - KdfIterations = preloginResponse.Result.KdfIterations; - } - - var result = new FullLoginResult(); - - var normalizedEmail = email.Trim().ToLower(); - var key = _cryptoService.MakeKeyFromPassword(masterPassword, normalizedEmail, Kdf, KdfIterations); - - var request = new TokenRequest - { - Email = normalizedEmail, - MasterPasswordHash = _cryptoService.HashPasswordBase64(key, masterPassword), - Device = new DeviceRequest(_appIdService, _deviceInfoService) - }; - - var twoFactorToken = _tokenService.GetTwoFactorToken(normalizedEmail); - if(!string.IsNullOrWhiteSpace(twoFactorToken)) - { - request.Token = twoFactorToken; - request.Provider = TwoFactorProviderType.Remember; - request.Remember = false; - } - - var response = await _connectApiRepository.PostTokenAsync(request); - if(!response.Succeeded) - { - result.Success = false; - result.ErrorMessage = response.Errors.FirstOrDefault()?.Message; - return result; - } - - result.Success = true; - if(response.Result.TwoFactorProviders2 != null && response.Result.TwoFactorProviders2.Count > 0) - { - result.Key = key; - result.MasterPasswordHash = request.MasterPasswordHash; - result.TwoFactorProviders = response.Result.TwoFactorProviders2; - return result; - } - - await ProcessLoginSuccessAsync(key, response.Result); - return result; - } - - public async Task TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, - string email, string masterPasswordHash, SymmetricCryptoKey key) - { - var result = new LoginResult(); - - var request = new TokenRequest - { - Remember = remember, - Email = email.Trim().ToLower(), - MasterPasswordHash = masterPasswordHash, - Token = token, - Provider = type, - Device = new DeviceRequest(_appIdService, _deviceInfoService) - }; - - var response = await _connectApiRepository.PostTokenAsync(request); - if(!response.Succeeded) - { - result.Success = false; - result.ErrorMessage = response.Errors.FirstOrDefault()?.Message; - return result; - } - - result.Success = true; - await ProcessLoginSuccessAsync(key, response.Result); - return result; - } - - private async Task ProcessLoginSuccessAsync(SymmetricCryptoKey key, TokenResponse response) - { - if(response.Key != null) - { - _cryptoService.SetEncKey(new CipherString(response.Key)); - } - - if(response.PrivateKey != null) - { - _cryptoService.SetPrivateKey(new CipherString(response.PrivateKey)); - } - - _cryptoService.Key = key; - _tokenService.Token = response.AccessToken; - _tokenService.RefreshToken = response.RefreshToken; - UserId = _tokenService.TokenUserId; - Email = _tokenService.TokenEmail; - _settings.AddOrUpdateValue(Constants.LastLoginEmail, Email); - _appSettingsService.FailedPinAttempts = 0; - _appSettingsService.OrganizationGivesPremium = false; - - if(response.PrivateKey != null) - { - var profile = await _accountsApiRepository.GetProfileAsync(); - if(profile.Succeeded) - { - _cryptoService.SetOrgKeys(profile.Result); - _appSettingsService.OrganizationGivesPremium = - profile.Result?.Organizations?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; - } - } - - if(!string.IsNullOrWhiteSpace(response.TwoFactorToken)) - { - _tokenService.SetTwoFactorToken(_tokenService.TokenEmail, response.TwoFactorToken); - } - } - } -} diff --git a/src/App/Services/CipherService.cs b/src/App/Services/CipherService.cs deleted file mode 100644 index d8aae7576..000000000 --- a/src/App/Services/CipherService.cs +++ /dev/null @@ -1,575 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Models.Api; -using Bit.App.Models.Data; -using System.Net.Http; -using Bit.App.Utilities; -using System.Text.RegularExpressions; -using Xamarin.Forms; - -namespace Bit.App.Services -{ - public class CipherService : ICipherService - { - public static List CachedCiphers = null; - - private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", - "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; - private readonly ICipherRepository _cipherRepository; - private readonly ICipherCollectionRepository _cipherCollectionRepository; - private readonly IAttachmentRepository _attachmentRepository; - private readonly IAuthService _authService; - private readonly ICipherApiRepository _cipherApiRepository; - private readonly ISettingsService _settingsService; - private readonly ICryptoService _cryptoService; - private readonly IAppSettingsService _appSettingsService; - private readonly IDeviceInfoService _deviceInfoService; - - public CipherService( - ICipherRepository cipherRepository, - ICipherCollectionRepository cipherCollectionRepository, - IAttachmentRepository attachmentRepository, - IAuthService authService, - ICipherApiRepository cipherApiRepository, - ISettingsService settingsService, - ICryptoService cryptoService, - IAppSettingsService appSettingsService, - IDeviceInfoService deviceInfoService) - { - _cipherRepository = cipherRepository; - _cipherCollectionRepository = cipherCollectionRepository; - _attachmentRepository = attachmentRepository; - _authService = authService; - _cipherApiRepository = cipherApiRepository; - _settingsService = settingsService; - _cryptoService = cryptoService; - _appSettingsService = appSettingsService; - _deviceInfoService = deviceInfoService; - } - - public async Task GetByIdAsync(string id) - { - var data = await _cipherRepository.GetByIdAsync(id); - if(data == null || data.UserId != _authService.UserId) - { - return null; - } - - var attachments = await _attachmentRepository.GetAllByCipherIdAsync(id); - var cipher = new Cipher(data, attachments); - return cipher; - } - - public async Task> GetAllAsync() - { - if(!_deviceInfoService.IsExtension && _appSettingsService.ClearCiphersCache) - { - CachedCiphers = null; - _appSettingsService.ClearCiphersCache = false; - } - - if(_deviceInfoService.IsExtension && _appSettingsService.ClearExtensionCiphersCache) - { - CachedCiphers = null; - _appSettingsService.ClearExtensionCiphersCache = false; - } - - if(CachedCiphers != null) - { - return CachedCiphers; - } - - var attachmentData = await _attachmentRepository.GetAllByUserIdAsync(_authService.UserId); - var attachmentDict = attachmentData.GroupBy(a => a.LoginId).ToDictionary(g => g.Key, g => g.ToList()); - var data = await _cipherRepository.GetAllByUserIdAsync(_authService.UserId); - CachedCiphers = data - .Select(f => new Cipher(f, attachmentDict.ContainsKey(f.Id) ? attachmentDict[f.Id] : null)) - .ToList(); - return CachedCiphers; - } - - public async Task> GetAllAsync(bool favorites) - { - var ciphers = await GetAllAsync(); - return ciphers.Where(c => c.Favorite == favorites); - } - - public async Task> GetAllByFolderAsync(string folderId) - { - var ciphers = await GetAllAsync(); - return ciphers.Where(c => c.FolderId == folderId); - } - - public async Task> GetAllByCollectionAsync(string collectionId) - { - var assoc = await _cipherCollectionRepository.GetAllByUserIdCollectionAsync(_authService.UserId, collectionId); - var cipherIds = new HashSet(assoc.Select(c => c.CipherId)); - var ciphers = await GetAllAsync(); - return ciphers.Where(c => cipherIds.Contains(c.Id)); - } - - public async Task, IEnumerable, IEnumerable>> GetAllAsync( - string uriString) - { - if(string.IsNullOrWhiteSpace(uriString)) - { - return null; - } - - string domainName = null; - var mobileApp = UriIsMobileApp(uriString); - - if(!mobileApp && - (!Uri.TryCreate(uriString, UriKind.Absolute, out Uri uri) || - !DomainName.TryParseBaseDomain(uri.Host, out domainName))) - { - return null; - } - - var mobileAppInfo = InfoFromMobileAppUri(uriString); - var mobileAppWebUriString = mobileAppInfo?.Item1; - var mobileAppSearchTerms = mobileAppInfo?.Item2; - var eqDomains = (await _settingsService.GetEquivalentDomainsAsync()).Select(d => d.ToArray()); - var matchingDomains = new List(); - var matchingFuzzyDomains = new List(); - foreach(var eqDomain in eqDomains) - { - if(mobileApp) - { - if(Array.IndexOf(eqDomain, uriString) >= 0) - { - matchingDomains.AddRange(eqDomain.Select(d => d).ToList()); - } - else if(mobileAppWebUriString != null && Array.IndexOf(eqDomain, mobileAppWebUriString) >= 0) - { - matchingFuzzyDomains.AddRange(eqDomain.Select(d => d).ToList()); - } - } - else if(Array.IndexOf(eqDomain, domainName) >= 0) - { - matchingDomains.AddRange(eqDomain.Select(d => d).ToList()); - } - } - - if(!matchingDomains.Any()) - { - matchingDomains.Add(mobileApp ? uriString : domainName); - } - - if(mobileApp && mobileAppWebUriString != null && - !matchingFuzzyDomains.Any() && !matchingDomains.Contains(mobileAppWebUriString)) - { - matchingFuzzyDomains.Add(mobileAppWebUriString); - } - - var matchingDomainsArray = matchingDomains.ToArray(); - var matchingFuzzyDomainsArray = matchingFuzzyDomains.ToArray(); - var matchingLogins = new List(); - var matchingFuzzyLogins = new List(); - var others = new List(); - var ciphers = await GetAllAsync(); - foreach(var cipher in ciphers) - { - if(cipher.Type != Enums.CipherType.Login) - { - others.Add(cipher); - continue; - } - - if(cipher.Login?.Uris == null || !cipher.Login.Uris.Any()) - { - continue; - } - - foreach(var u in cipher.Login.Uris) - { - var loginUriString = u.Uri?.Decrypt(cipher.OrganizationId); - if(string.IsNullOrWhiteSpace(loginUriString)) - { - break; - } - - var match = false; - switch(u.Match) - { - case null: - case Enums.UriMatchType.Domain: - match = CheckDefaultUriMatch(cipher, loginUriString, matchingLogins, matchingFuzzyLogins, - matchingDomainsArray, matchingFuzzyDomainsArray, mobileApp, mobileAppSearchTerms); - break; - case Enums.UriMatchType.Host: - var urlHost = Helpers.GetUrlHost(uriString); - match = urlHost != null && urlHost == Helpers.GetUrlHost(loginUriString); - if(match) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - } - break; - case Enums.UriMatchType.Exact: - match = uriString == loginUriString; - if(match) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - } - break; - case Enums.UriMatchType.StartsWith: - match = uriString.StartsWith(loginUriString); - if(match) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - } - break; - case Enums.UriMatchType.RegularExpression: - var regex = new Regex(loginUriString, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); - match = regex.IsMatch(uriString); - if(match) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - } - break; - case Enums.UriMatchType.Never: - default: - break; - } - - if(match) - { - break; - } - } - } - - return new Tuple, IEnumerable, IEnumerable>( - matchingLogins, matchingFuzzyLogins, others); - } - - public async Task> SaveAsync(Cipher cipher) - { - ApiResult response = null; - var request = new CipherRequest(cipher); - - if(cipher.Id == null) - { - response = await _cipherApiRepository.PostAsync(request); - } - else - { - response = await _cipherApiRepository.PutAsync(cipher.Id, request); - } - - if(response.Succeeded) - { - var data = new CipherData(response.Result, _authService.UserId); - await UpsertDataAsync(data, true, cipher.Id == null); - cipher.Id = data.Id; - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - - public async Task UpsertDataAsync(CipherData cipher, bool sendMessage, bool created) - { - await _cipherRepository.UpsertAsync(cipher); - CachedCiphers = null; - _appSettingsService.ClearCiphersCache = true; - _appSettingsService.ClearExtensionCiphersCache = true; - if(sendMessage && Application.Current != null) - { - MessagingCenter.Send(Application.Current, "UpsertedCipher", - new Tuple(cipher.Id, created)); - } - } - - public async Task DeleteAsync(string id) - { - var response = await _cipherApiRepository.DeleteAsync(id); - if(response.Succeeded) - { - await DeleteDataAsync(id, true); - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - - public async Task DeleteDataAsync(string id, bool sendMessage) - { - if(sendMessage) - { - var cipherData = await _cipherRepository.GetByIdAsync(id); - if(cipherData != null && Application.Current != null) - { - MessagingCenter.Send(Application.Current, "DeletedCipher", new Cipher(cipherData)); - } - } - await _cipherRepository.DeleteAsync(id); - CachedCiphers = null; - _appSettingsService.ClearCiphersCache = true; - _appSettingsService.ClearExtensionCiphersCache = true; - } - - public async Task DownloadAndDecryptAttachmentAsync(string url, CipherString key, string orgId = null) - { - using(var client = new HttpClient()) - { - try - { - var response = await client.GetAsync(new Uri(url)).ConfigureAwait(false); - if(!response.IsSuccessStatusCode) - { - return null; - } - - var data = await response.Content.ReadAsByteArrayAsync(); - if(data == null) - { - return null; - } - - SymmetricCryptoKey regularKey = !string.IsNullOrWhiteSpace(orgId) ? - _cryptoService.GetOrgKey(orgId) : null; - SymmetricCryptoKey dataKey = null; - if(key != null) - { - var decDataKey = _cryptoService.DecryptToBytes(key, regularKey); - dataKey = new SymmetricCryptoKey(decDataKey); - } - else - { - dataKey = regularKey; - } - - return _cryptoService.DecryptToBytes(data, dataKey); - } - catch - { - return null; - } - } - } - - public async Task> EncryptAndSaveAttachmentAsync(Cipher cipher, byte[] data, string fileName) - { - var key = cipher.OrganizationId != null ? _cryptoService.GetOrgKey(cipher.OrganizationId) : null; - var encFileName = fileName.Encrypt(cipher.OrganizationId); - - var dataKey = _cryptoService.MakeEncKey(key); - var encBytes = _cryptoService.EncryptToBytes(data, dataKey.Item1); - var response = await _cipherApiRepository.PostAttachmentAsync(cipher.Id, encBytes, - dataKey.Item2.EncryptedString, encFileName.EncryptedString); - - if(response.Succeeded) - { - var attachmentData = response.Result.Attachments.Select(a => new AttachmentData(a, cipher.Id)); - await UpsertAttachmentDataAsync(attachmentData); - cipher.Attachments = response.Result.Attachments.Select(a => new Attachment(a)); - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - - public async Task UpsertAttachmentDataAsync(IEnumerable attachments) - { - foreach(var attachment in attachments) - { - await _attachmentRepository.UpsertAsync(attachment); - } - CachedCiphers = null; - _appSettingsService.ClearCiphersCache = true; - _appSettingsService.ClearExtensionCiphersCache = true; - } - - public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId) - { - var response = await _cipherApiRepository.DeleteAttachmentAsync(cipher.Id, attachmentId); - if(response.Succeeded) - { - await DeleteAttachmentDataAsync(attachmentId); - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - - public async Task DeleteAttachmentDataAsync(string attachmentId) - { - await _attachmentRepository.DeleteAsync(attachmentId); - CachedCiphers = null; - _appSettingsService.ClearCiphersCache = true; - _appSettingsService.ClearExtensionCiphersCache = true; - } - - private Tuple InfoFromMobileAppUri(string mobileAppUriString) - { - if(UriIsAndroidApp(mobileAppUriString)) - { - return InfoFromAndroidAppUri(mobileAppUriString); - } - else if(UriIsiOSApp(mobileAppUriString)) - { - return InfoFromiOSAppUri(mobileAppUriString); - } - - return null; - } - - private Tuple InfoFromAndroidAppUri(string androidAppUriString) - { - if(!UriIsAndroidApp(androidAppUriString)) - { - return null; - } - - var androidUriParts = androidAppUriString.Replace(Constants.AndroidAppProtocol, string.Empty).Split('.'); - if(androidUriParts.Length >= 2) - { - var webUri = string.Join(".", androidUriParts[1], androidUriParts[0]); - var searchTerms = androidUriParts.Where(p => !_ignoredSearchTerms.Contains(p)) - .Select(p => p.ToLowerInvariant()).ToArray(); - return new Tuple(webUri, searchTerms); - } - - return null; - } - - private Tuple InfoFromiOSAppUri(string iosAppUriString) - { - if(!UriIsiOSApp(iosAppUriString)) - { - return null; - } - - var webUri = iosAppUriString.Replace(Constants.iOSAppProtocol, string.Empty); - return new Tuple(webUri, null); - } - - private bool UriIsMobileApp(string uriString) - { - return UriIsAndroidApp(uriString) || UriIsiOSApp(uriString); - } - - private bool UriIsAndroidApp(string uriString) - { - return uriString.StartsWith(Constants.AndroidAppProtocol); - } - - private bool UriIsiOSApp(string uriString) - { - return uriString.StartsWith(Constants.iOSAppProtocol); - } - - private bool CheckDefaultUriMatch(Cipher cipher, string loginUriString, List matchingLogins, - List matchingFuzzyLogins, string[] matchingDomainsArray, string[] matchingFuzzyDomainsArray, - bool mobileApp, string[] mobileAppSearchTerms) - { - if(Array.IndexOf(matchingDomainsArray, loginUriString) >= 0) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - return true; - } - else if(mobileApp && Array.IndexOf(matchingFuzzyDomainsArray, loginUriString) >= 0) - { - AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); - return false; - } - else if(!mobileApp) - { - var info = InfoFromMobileAppUri(loginUriString); - if(info?.Item1 != null && Array.IndexOf(matchingDomainsArray, info.Item1) >= 0) - { - AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); - return false; - } - } - - if(!loginUriString.Contains("://") && loginUriString.Contains(".")) - { - loginUriString = "http://" + loginUriString; - } - string loginDomainName = null; - if(Uri.TryCreate(loginUriString, UriKind.Absolute, out Uri loginUri) - && DomainName.TryParseBaseDomain(loginUri.Host, out loginDomainName)) - { - loginDomainName = loginDomainName.ToLowerInvariant(); - - if(Array.IndexOf(matchingDomainsArray, loginDomainName) >= 0) - { - AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); - return true; - } - else if(mobileApp && Array.IndexOf(matchingFuzzyDomainsArray, loginDomainName) >= 0) - { - AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); - return false; - } - } - - if(mobileApp && mobileAppSearchTerms != null && mobileAppSearchTerms.Length > 0) - { - var addedFromSearchTerm = false; - var loginNameString = cipher.Name == null ? null : - cipher.Name.Decrypt(cipher.OrganizationId)?.ToLowerInvariant(); - foreach(var term in mobileAppSearchTerms) - { - addedFromSearchTerm = (loginDomainName != null && loginDomainName.Contains(term)) || - (loginNameString != null && loginNameString.Contains(term)); - if(!addedFromSearchTerm) - { - var domainTerm = loginDomainName?.Split('.')[0]; - addedFromSearchTerm = - (domainTerm != null && domainTerm.Length > 2 && term.Contains(domainTerm)) || - (loginNameString != null && term.Contains(loginNameString)); - } - - if(addedFromSearchTerm) - { - AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); - return false; - } - } - } - - return false; - } - - private void AddMatchingLogin(Cipher cipher, List matchingLogins, List matchingFuzzyLogins) - { - if(matchingFuzzyLogins.Contains(cipher)) - { - matchingFuzzyLogins.Remove(cipher); - } - - matchingLogins.Add(cipher); - } - - private void AddMatchingFuzzyLogin(Cipher cipher, List matchingLogins, List matchingFuzzyLogins) - { - if(!matchingFuzzyLogins.Contains(cipher) && !matchingLogins.Contains(cipher)) - { - matchingFuzzyLogins.Add(cipher); - } - } - } -} diff --git a/src/App/Services/CollectionService.cs b/src/App/Services/CollectionService.cs deleted file mode 100644 index 009c101d9..000000000 --- a/src/App/Services/CollectionService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models; - -namespace Bit.App.Services -{ - public class CollectionService : ICollectionService - { - private readonly ICollectionRepository _collectionRepository; - private readonly ICipherCollectionRepository _cipherCollectionRepository; - private readonly IAuthService _authService; - - public CollectionService( - ICollectionRepository collectionRepository, - ICipherCollectionRepository cipherCollectionRepository, - IAuthService authService) - { - _collectionRepository = collectionRepository; - _cipherCollectionRepository = cipherCollectionRepository; - _authService = authService; - } - - public async Task GetByIdAsync(string id) - { - var data = await _collectionRepository.GetByIdAsync(id); - if(data == null || data.UserId != _authService.UserId) - { - return null; - } - - var collection = new Collection(data); - return collection; - } - - public async Task> GetAllAsync() - { - var data = await _collectionRepository.GetAllByUserIdAsync(_authService.UserId); - var collections = data.Select(c => new Collection(c)); - return collections; - } - - public async Task>> GetAllCipherAssociationsAsync() - { - var data = await _cipherCollectionRepository.GetAllByUserIdAsync(_authService.UserId); - var assocs = data.Select(cc => new Tuple(cc.CipherId, cc.CollectionId)); - return assocs; - } - } -} diff --git a/src/App/Services/CryptoService.cs b/src/App/Services/CryptoService.cs deleted file mode 100644 index 633f4545d..000000000 --- a/src/App/Services/CryptoService.cs +++ /dev/null @@ -1,518 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text; -using Bit.App.Abstractions; -using Bit.App.Models; -using PCLCrypto; -using System.Linq; -using Bit.App.Enums; -using System.Collections.Generic; -using Newtonsoft.Json; -using Plugin.Settings.Abstractions; -using Bit.App.Models.Api; -using Bit.App.Utilities; - -namespace Bit.App.Services -{ - public class CryptoService : ICryptoService - { - private const string KeyKey = "key"; - private const string PrivateKeyKey = "encPrivateKey"; - private const string EncKeyKey = "encKey"; - private const string OrgKeysKey = "encOrgKeys"; - private const int InitializationVectorSize = 16; - - private readonly ISettings _settings; - private readonly ISecureStorageService _secureStorage; - private readonly IKeyDerivationService _keyDerivationService; - private SymmetricCryptoKey _key; - private SymmetricCryptoKey _encKey; - private SymmetricCryptoKey _legacyEtmKey; - private IDictionary _orgKeys; - private byte[] _privateKey; - - public CryptoService( - ISettings settings, - ISecureStorageService secureStorage, - IKeyDerivationService keyDerivationService) - { - _settings = settings; - _secureStorage = secureStorage; - _keyDerivationService = keyDerivationService; - } - - public SymmetricCryptoKey Key - { - get - { - if(_key == null && _secureStorage.Contains(KeyKey)) - { - var keyBytes = _secureStorage.Retrieve(KeyKey); - if(keyBytes != null) - { - _key = new SymmetricCryptoKey(keyBytes); - } - } - - return _key; - } - set - { - if(value != null) - { - _secureStorage.Store(KeyKey, value.Key); - } - else - { - _secureStorage.Delete(KeyKey); - } - - _key = value; - _legacyEtmKey = null; - } - } - - public SymmetricCryptoKey EncKey - { - get - { - if(_encKey == null && _settings.Contains(EncKeyKey)) - { - var encKey = _settings.GetValueOrDefault(EncKeyKey, null); - var encKeyCs = new CipherString(encKey); - try - { - byte[] decEncKey = null; - if(encKeyCs.EncryptionType == EncryptionType.AesCbc256_B64) - { - decEncKey = DecryptToBytes(encKeyCs, Key); - } - else if(encKeyCs.EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64) - { - var newKey = StretchKey(Key); - decEncKey = DecryptToBytes(encKeyCs, newKey); - } - else - { - throw new Exception("Unsupported EncKey type"); - } - - if(decEncKey != null) - { - _encKey = new SymmetricCryptoKey(decEncKey); - } - } - catch - { - _encKey = null; - Debug.WriteLine($"Cannot set enc key. Decryption failed."); - } - } - - return _encKey; - } - } - - public byte[] PrivateKey - { - get - { - if(_privateKey == null && _settings.Contains(PrivateKeyKey)) - { - var encPrivateKey = _settings.GetValueOrDefault(PrivateKeyKey, null); - var encPrivateKeyCs = new CipherString(encPrivateKey); - try - { - _privateKey = DecryptToBytes(encPrivateKeyCs); - } - catch - { - _privateKey = null; - Debug.WriteLine($"Cannot set private key. Decryption failed."); - } - } - - return _privateKey; - } - } - - public IDictionary OrgKeys - { - get - { - if((!_orgKeys?.Any() ?? true) && _settings.Contains(OrgKeysKey)) - { - var orgKeysEncDictJson = _settings.GetValueOrDefault(OrgKeysKey, null); - if(!string.IsNullOrWhiteSpace(orgKeysEncDictJson)) - { - _orgKeys = new Dictionary(); - var orgKeysDict = JsonConvert.DeserializeObject>(orgKeysEncDictJson); - foreach(var item in orgKeysDict) - { - try - { - var orgKeyCs = new CipherString(item.Value); - var decOrgKeyBytes = RsaDecryptToBytes(orgKeyCs, PrivateKey); - _orgKeys.Add(item.Key, new SymmetricCryptoKey(decOrgKeyBytes)); - } - catch - { - Debug.WriteLine($"Cannot set org key {item.Key}. Decryption failed."); - } - } - } - } - - return _orgKeys; - } - } - - public void SetEncKey(CipherString encKeyEnc) - { - if(encKeyEnc != null) - { - _settings.AddOrUpdateValue(EncKeyKey, encKeyEnc.EncryptedString); - } - else if(_settings.Contains(EncKeyKey)) - { - _settings.Remove(EncKeyKey); - } - - _encKey = null; - } - - public void SetPrivateKey(CipherString privateKeyEnc) - { - if(privateKeyEnc != null) - { - _settings.AddOrUpdateValue(PrivateKeyKey, privateKeyEnc.EncryptedString); - } - else if(_settings.Contains(PrivateKeyKey)) - { - _settings.Remove(PrivateKeyKey); - } - - _privateKey = null; - } - - public void SetOrgKeys(ProfileResponse profile) - { - var orgKeysEncDict = new Dictionary(); - - if(profile?.Organizations?.Any() ?? false) - { - foreach(var org in profile.Organizations) - { - orgKeysEncDict.Add(org.Id, org.Key); - } - } - - SetOrgKeys(orgKeysEncDict); - } - - public void SetOrgKeys(Dictionary orgKeysEncDict) - { - if(orgKeysEncDict?.Any() ?? false) - { - var dictJson = JsonConvert.SerializeObject(orgKeysEncDict); - _settings.AddOrUpdateValue(OrgKeysKey, dictJson); - } - else if(_settings.Contains(OrgKeysKey)) - { - _settings.Remove(OrgKeysKey); - } - - _orgKeys = null; - } - - public SymmetricCryptoKey GetOrgKey(string orgId) - { - if(OrgKeys == null || !OrgKeys.ContainsKey(orgId)) - { - return null; - } - - return OrgKeys[orgId]; - } - - public void ClearKeys() - { - SetOrgKeys((Dictionary)null); - Key = null; - SetPrivateKey(null); - SetEncKey(null); - } - - public CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null) - { - if(plaintextValue == null) - { - throw new ArgumentNullException(nameof(plaintextValue)); - } - - var plaintextBytes = Encoding.UTF8.GetBytes(plaintextValue); - return Encrypt(plaintextBytes, key); - } - - public CipherString Encrypt(byte[] plainBytes, SymmetricCryptoKey key = null) - { - if(key == null) - { - key = EncKey ?? Key; - } - - if(key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if(plainBytes == null) - { - throw new ArgumentNullException(nameof(plainBytes)); - } - - return Crypto.AesCbcEncrypt(plainBytes, key); - } - - public byte[] EncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key = null) - { - if(key == null) - { - key = EncKey ?? Key; - } - - if(key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if(plainBytes == null) - { - throw new ArgumentNullException(nameof(plainBytes)); - } - - return Crypto.AesCbcEncryptToBytes(plainBytes, key); - } - - public string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null) - { - try - { - var bytes = DecryptToBytes(encyptedValue, key); - return Encoding.UTF8.GetString(bytes, 0, bytes.Length).TrimEnd('\0'); - } - catch(Exception e) - { - Debug.WriteLine("Could not decrypt '{0}'. {1}", encyptedValue, e.Message); - return "[error: cannot decrypt]"; - } - } - - public byte[] DecryptToBytes(CipherString encyptedValue, SymmetricCryptoKey key = null) - { - if(key == null) - { - key = EncKey ?? Key; - } - - if(key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if(encyptedValue == null) - { - throw new ArgumentNullException(nameof(encyptedValue)); - } - - if(encyptedValue.EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && - key.EncryptionType == EncryptionType.AesCbc256_B64) - { - // Old encrypt-then-mac scheme, swap out the key - if(_legacyEtmKey == null) - { - _legacyEtmKey = new SymmetricCryptoKey(key.Key, EncryptionType.AesCbc128_HmacSha256_B64); - } - - key = _legacyEtmKey; - } - - return Crypto.AesCbcDecrypt(encyptedValue, key); - } - - public byte[] DecryptToBytes(byte[] encyptedValue, SymmetricCryptoKey key = null) - { - if(key == null) - { - key = EncKey ?? Key; - } - - if(key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if(encyptedValue == null || encyptedValue.Length == 0) - { - throw new ArgumentNullException(nameof(encyptedValue)); - } - - byte[] ct, iv, mac = null; - var encType = (EncryptionType)encyptedValue[0]; - switch(encType) - { - case EncryptionType.AesCbc128_HmacSha256_B64: - case EncryptionType.AesCbc256_HmacSha256_B64: - if(encyptedValue.Length <= 49) - { - throw new InvalidOperationException("Invalid value length."); - } - - iv = new ArraySegment(encyptedValue, 1, 16).ToArray(); - mac = new ArraySegment(encyptedValue, 17, 32).ToArray(); - ct = new ArraySegment(encyptedValue, 49, encyptedValue.Length - 49).ToArray(); - break; - case EncryptionType.AesCbc256_B64: - if(encyptedValue.Length <= 17) - { - throw new InvalidOperationException("Invalid value length."); - } - - iv = new ArraySegment(encyptedValue, 1, 16).ToArray(); - ct = new ArraySegment(encyptedValue, 17, encyptedValue.Length - 17).ToArray(); - break; - default: - throw new InvalidOperationException("Invalid encryption type."); - } - - return Crypto.AesCbcDecrypt(encType, ct, iv, mac, key); - } - - public byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey) - { - if(privateKey == null) - { - privateKey = PrivateKey; - } - - if(privateKey == null) - { - throw new ArgumentNullException(nameof(privateKey)); - } - - IAsymmetricKeyAlgorithmProvider provider = null; - switch(encyptedValue.EncryptionType) - { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - provider = WinRTCrypto.AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha256); - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - provider = WinRTCrypto.AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); - break; - default: - throw new ArgumentException("EncryptionType unavailable."); - } - - var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); - var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, encyptedValue.CipherTextBytes); - return decryptedBytes; - } - - public SymmetricCryptoKey MakeKeyFromPassword(string password, string salt, KdfType kdf, int kdfIterations) - { - if(password == null) - { - throw new ArgumentNullException(nameof(password)); - } - - if(salt == null) - { - throw new ArgumentNullException(nameof(salt)); - } - - var passwordBytes = Encoding.UTF8.GetBytes(NormalizePassword(password)); - var saltBytes = Encoding.UTF8.GetBytes(salt); - - byte[] keyBytes = null; - if(kdf == KdfType.PBKDF2_SHA256) - { - if(kdfIterations < 5000) - { - throw new Exception("PBKDF2 iteration minimum is 5000."); - } - keyBytes = _keyDerivationService.DeriveKey(passwordBytes, saltBytes, (uint)kdfIterations); - } - else - { - throw new Exception("Unknown Kdf."); - } - return new SymmetricCryptoKey(keyBytes); - } - - public byte[] HashPassword(SymmetricCryptoKey key, string password) - { - if(key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if(password == null) - { - throw new ArgumentNullException(nameof(password)); - } - - var passwordBytes = Encoding.UTF8.GetBytes(NormalizePassword(password)); - var hash = _keyDerivationService.DeriveKey(key.Key, passwordBytes, 1); - return hash; - } - - public string HashPasswordBase64(SymmetricCryptoKey key, string password) - { - var hash = HashPassword(key, password); - return Convert.ToBase64String(hash); - } - - public Tuple MakeEncKey(SymmetricCryptoKey key) - { - var theKey = key ?? EncKey ?? Key; - var encKey = Crypto.RandomBytes(64); - if(theKey.Key.Length == 32) - { - var newKey = StretchKey(theKey); - return new Tuple( - new SymmetricCryptoKey(encKey), Encrypt(encKey, newKey)); - } - else if(theKey.Key.Length == 64) - { - return new Tuple( - new SymmetricCryptoKey(encKey), Encrypt(encKey, theKey)); - } - - throw new Exception("Invalid key size."); - } - - // Some users like to copy/paste passwords from external files. Sometimes this can lead to two different - // values on mobiles apps vs the web. For example, on Android an EditText will accept a new line character - // (\n), whereas whenever you paste a new line character on the web in a HTML input box it is converted - // to a space ( ). Normalize those values so that they are the same on all platforms. - private string NormalizePassword(string password) - { - return password - .Replace("\r\n", " ") // Windows-style new line => space - .Replace("\n", " ") // New line => space - .Replace(" ", " "); // No-break space (00A0) => space - } - - private SymmetricCryptoKey StretchKey(SymmetricCryptoKey key) - { - var newKey = new byte[64]; - var encKey = Crypto.HkdfExpand(key.Key, Encoding.UTF8.GetBytes("enc"), 32); - var macKey = Crypto.HkdfExpand(key.Key, Encoding.UTF8.GetBytes("mac"), 32); - encKey.CopyTo(newKey, 0); - macKey.CopyTo(newKey, 32); - return new SymmetricCryptoKey(newKey); - } - } -} diff --git a/src/App/Services/DatabaseService.cs b/src/App/Services/DatabaseService.cs deleted file mode 100644 index 3f177c6eb..000000000 --- a/src/App/Services/DatabaseService.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Models.Data; -using SQLite; - -namespace Bit.App.Services -{ - public class DatabaseService : IDatabaseService - { - protected readonly SQLiteConnection _connection; - - public DatabaseService(ISqlService sqlService) - { - _connection = sqlService.GetConnection(); - } - - public void CreateTables() - { - _connection.CreateTable(); - _connection.CreateTable(); - _connection.CreateTable(); - _connection.CreateTable(); - _connection.CreateTable(); - _connection.CreateTable(); - } - } -} diff --git a/src/App/Services/FolderService.cs b/src/App/Services/FolderService.cs deleted file mode 100644 index 7a7c73666..000000000 --- a/src/App/Services/FolderService.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Models.Data; -using Bit.App.Models.Api; -using Xamarin.Forms; - -namespace Bit.App.Services -{ - public class FolderService : IFolderService - { - private readonly IFolderRepository _folderRepository; - private readonly IAuthService _authService; - private readonly IFolderApiRepository _folderApiRepository; - - public FolderService( - IFolderRepository folderRepository, - IAuthService authService, - IFolderApiRepository folderApiRepository) - { - _folderRepository = folderRepository; - _authService = authService; - _folderApiRepository = folderApiRepository; - } - - public async Task GetByIdAsync(string id) - { - var data = await _folderRepository.GetByIdAsync(id); - if(data == null || data.UserId != _authService.UserId) - { - return null; - } - - var folder = new Folder(data); - return folder; - } - - public async Task> GetAllAsync() - { - var data = await _folderRepository.GetAllByUserIdAsync(_authService.UserId); - var folders = data.Select(f => new Folder(f)); - return folders; - } - - public async Task> SaveAsync(Folder folder) - { - ApiResult response = null; - var request = new FolderRequest(folder); - - if(folder.Id == null) - { - response = await _folderApiRepository.PostAsync(request); - } - else - { - response = await _folderApiRepository.PutAsync(folder.Id, request); - } - - if(response.Succeeded) - { - var data = new FolderData(response.Result, _authService.UserId); - if(folder.Id == null) - { - await _folderRepository.InsertAsync(data); - folder.Id = data.Id; - } - else - { - await _folderRepository.UpdateAsync(data); - } - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - - public async Task DeleteAsync(string folderId) - { - var response = await _folderApiRepository.DeleteAsync(folderId); - if(response.Succeeded) - { - await _folderRepository.DeleteAsync(folderId); - } - else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden - || response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _authService.LogOut(); - } - - return response; - } - } -} diff --git a/src/App/Services/LockService.cs b/src/App/Services/LockService.cs deleted file mode 100644 index 8002dfc75..000000000 --- a/src/App/Services/LockService.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Plugin.Settings.Abstractions; -using Plugin.Fingerprint.Abstractions; -using Bit.App.Enums; -using System.Threading.Tasks; -using Bit.App.Controls; -using Bit.App.Pages; -using Xamarin.Forms; -using System.Linq; -using System.Diagnostics; - -namespace Bit.App.Services -{ - public class LockService : ILockService - { - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettings; - private readonly IAuthService _authService; - private readonly IFingerprint _fingerprint; - private Stopwatch _stopwatch = null; - - public LockService( - ISettings settings, - IAppSettingsService appSettings, - IAuthService authService, - IFingerprint fingerprint) - { - _settings = settings; - _appSettings = appSettings; - _authService = authService; - _fingerprint = fingerprint; - } - - public void UpdateLastActivity() - { - _stopwatch?.Restart(); - } - - public async Task GetLockTypeAsync(bool forceLock, bool onlyIfAlreadyLocked = false) - { - // Only lock if they are logged in - if(!_authService.IsAuthenticated) - { - return LockType.None; - } - - // Are we forcing a lock? (i.e. clicking a button to lock the app manually, immediately) - if(!forceLock && !_appSettings.Locked) - { - // Lock seconds tells if they want to lock the app or not - var lockSeconds = _settings.GetValueOrDefault(Constants.SettingLockSeconds, 60 * 15); - var neverLock = lockSeconds == -1; - - // Has it been longer than lockSeconds since the last time the app was used? - if(neverLock || (_stopwatch != null && _stopwatch.Elapsed.TotalSeconds < lockSeconds)) - { - return LockType.None; - } - } - - if(onlyIfAlreadyLocked && !_appSettings.Locked) - { - return LockType.None; - } - - // What method are we using to unlock? - var fingerprintUnlock = _settings.GetValueOrDefault(Constants.SettingFingerprintUnlockOn, false); - var pinUnlock = _settings.GetValueOrDefault(Constants.SettingPinUnlockOn, false); - var fingerprintAvailability = await _fingerprint.GetAvailabilityAsync(); - if(fingerprintUnlock && fingerprintAvailability == FingerprintAvailability.Available) - { - return LockType.Fingerprint; - } - else if(pinUnlock && !string.IsNullOrWhiteSpace(_authService.PIN)) - { - return LockType.PIN; - } - else - { - return LockType.Password; - } - } - - public async Task CheckLockAsync(bool forceLock, bool onlyIfAlreadyLocked = false) - { - if(TopPageIsLock()) - { - return; - } - - var lockType = await GetLockTypeAsync(forceLock, onlyIfAlreadyLocked); - if(lockType == LockType.None) - { - return; - } - - if(_stopwatch == null) - { - _stopwatch = Stopwatch.StartNew(); - } - - _appSettings.Locked = true; - switch(lockType) - { - case LockType.Fingerprint: - await Application.Current.MainPage.Navigation.PushModalAsync( - new ExtendedNavigationPage(new LockFingerprintPage(!forceLock)), false); - break; - case LockType.PIN: - await Application.Current.MainPage.Navigation.PushModalAsync( - new ExtendedNavigationPage(new LockPinPage()), false); - break; - case LockType.Password: - await Application.Current.MainPage.Navigation.PushModalAsync( - new ExtendedNavigationPage(new LockPasswordPage()), false); - break; - default: - break; - } - } - - public bool TopPageIsLock() - { - var currentPage = Application.Current.MainPage.Navigation.ModalStack.LastOrDefault() as ExtendedNavigationPage; - if((currentPage?.CurrentPage as LockFingerprintPage) != null) - { - return true; - } - if((currentPage?.CurrentPage as LockPinPage) != null) - { - return true; - } - if((currentPage?.CurrentPage as LockPasswordPage) != null) - { - return true; - } - - return false; - } - } -} diff --git a/src/App/Services/MobileBroadcasterMessagingService.cs b/src/App/Services/MobileBroadcasterMessagingService.cs new file mode 100644 index 000000000..94c1908b1 --- /dev/null +++ b/src/App/Services/MobileBroadcasterMessagingService.cs @@ -0,0 +1,21 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; + +namespace Bit.App.Services +{ + public class MobileBroadcasterMessagingService : IMessagingService + { + private readonly IBroadcasterService _broadcasterService; + + public MobileBroadcasterMessagingService(IBroadcasterService broadcasterService) + { + _broadcasterService = broadcasterService; + } + + public void Send(string subscriber, object arg = null) + { + var message = new Message { Command = subscriber, Data = arg }; + _broadcasterService.Send(message); + } + } +} diff --git a/src/App/Services/MobileI18nService.cs b/src/App/Services/MobileI18nService.cs new file mode 100644 index 000000000..ba2c3ed95 --- /dev/null +++ b/src/App/Services/MobileI18nService.cs @@ -0,0 +1,91 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using System; +using System.Globalization; +using System.Reflection; +using System.Resources; +using System.Threading; + +namespace Bit.App.Services +{ + public class MobileI18nService : II18nService + { + private const string ResourceId = "Bit.App.Resources.AppResources"; + + private static readonly Lazy _resourceManager = new Lazy(() => + new ResourceManager(ResourceId, IntrospectionExtensions.GetTypeInfo(typeof(MobileI18nService)).Assembly)); + + private readonly CultureInfo _defaultCulture = new CultureInfo("en-US"); + private bool _inited; + private StringComparer _stringComparer; + + public MobileI18nService(CultureInfo systemCulture) + { + Culture = systemCulture; + } + + public CultureInfo Culture { get; set; } + public StringComparer StringComparer + { + get + { + if(_stringComparer == null) + { + _stringComparer = StringComparer.Create(Culture, false); + } + return _stringComparer; + } + } + + public void Init(CultureInfo culture = null) + { + if(_inited) + { + throw new Exception("I18n already inited."); + } + _inited = true; + if(culture != null) + { + Culture = culture; + } + AppResources.Culture = Culture; + Thread.CurrentThread.CurrentCulture = Culture; + Thread.CurrentThread.CurrentUICulture = Culture; + } + + public string T(string id, string p1 = null, string p2 = null, string p3 = null) + { + return Translate(id, p1, p2, p3); + } + + public string Translate(string id, string p1 = null, string p2 = null, string p3 = null) + { + if(string.IsNullOrWhiteSpace(id)) + { + return string.Empty; + } + var result = _resourceManager.Value.GetString(id, Culture); + if(result == null) + { + result = _resourceManager.Value.GetString(id, _defaultCulture); + if(result == null) + { + result = $"{{{id}}}"; + } + } + if(p1 == null && p2 == null && p3 == null) + { + return result; + } + else if(p2 == null && p3 == null) + { + return string.Format(result, p1); + } + else if(p3 == null) + { + return string.Format(result, p1, p2); + } + return string.Format(result, p1, p2, p3); + } + } +} diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs new file mode 100644 index 000000000..f0f6c31da --- /dev/null +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -0,0 +1,233 @@ +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Plugin.Fingerprint; +using Plugin.Fingerprint.Abstractions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xamarin.Essentials; +using Xamarin.Forms; + +namespace Bit.App.Services +{ + public class MobilePlatformUtilsService : IPlatformUtilsService + { + private static readonly Random _random = new Random(); + + private const int DialogPromiseExpiration = 600000; // 10 minutes + + private readonly IDeviceActionService _deviceActionService; + private readonly IMessagingService _messagingService; + private readonly IBroadcasterService _broadcasterService; + private readonly Dictionary, DateTime>> _showDialogResolves = + new Dictionary, DateTime>>(); + + public MobilePlatformUtilsService( + IDeviceActionService deviceActionService, + IMessagingService messagingService, + IBroadcasterService broadcasterService) + { + _deviceActionService = deviceActionService; + _messagingService = messagingService; + _broadcasterService = broadcasterService; + } + + public string IdentityClientId => "mobile"; + + public void Init() + { + _broadcasterService.Subscribe(nameof(MobilePlatformUtilsService), (message) => + { + if(message.Command == "showDialogResolve") + { + var details = message.Data as Tuple; + var dialogId = details.Item1; + var confirmed = details.Item2; + if(_showDialogResolves.ContainsKey(dialogId)) + { + var resolveObj = _showDialogResolves[dialogId].Item1; + resolveObj.TrySetResult(confirmed); + } + + // Clean up old tasks + var deleteIds = new HashSet(); + foreach(var item in _showDialogResolves) + { + var age = DateTime.UtcNow - item.Value.Item2; + if(age.TotalMilliseconds > DialogPromiseExpiration) + { + deleteIds.Add(item.Key); + } + } + foreach(var id in deleteIds) + { + _showDialogResolves.Remove(id); + } + } + }); + } + + public Core.Enums.DeviceType GetDevice() + { + return _deviceActionService.DeviceType; + } + + public string GetDeviceString() + { + return DeviceInfo.Model; + } + + public bool IsViewOpen() + { + return false; + } + + public int? LockTimeout() + { + return null; + } + + public void LaunchUri(string uri, Dictionary options = null) + { + if(uri.StartsWith("http://") || uri.StartsWith("https://")) + { + Browser.OpenAsync(uri, BrowserLaunchMode.External); + } + else + { + var launched = false; + if(GetDevice() == Core.Enums.DeviceType.Android && uri.StartsWith("androidapp://")) + { + launched = _deviceActionService.LaunchApp(uri); + } + if(!launched && (options?.ContainsKey("page") ?? false)) + { + (options["page"] as Page).DisplayAlert(null, "", ""); // TODO + } + } + } + + public void SaveFile() + { + // TODO + } + + public string GetApplicationVersion() + { + return AppInfo.VersionString; + } + + public bool SupportsDuo() + { + return true; + } + + public bool SupportsU2f() + { + return false; + } + + public void ShowToast(string type, string title, string text, Dictionary options = null) + { + ShowToast(type, title, new string[] { text }, options); + } + + public void ShowToast(string type, string title, string[] text, Dictionary options = null) + { + if(text.Length > 0) + { + var longDuration = options != null && options.ContainsKey("longDuration") ? + (bool)options["longDuration"] : false; + _deviceActionService.Toast(text[0], longDuration); + } + } + + public Task ShowDialogAsync(string text, string title = null, string confirmText = null, + string cancelText = null, string type = null) + { + var dialogId = 0; + lock(_random) + { + dialogId = _random.Next(0, int.MaxValue); + } + _messagingService.Send("showDialog", new DialogDetails + { + Text = text, + Title = title, + ConfirmText = confirmText, + CancelText = cancelText, + Type = type, + DialogId = dialogId + }); + var tcs = new TaskCompletionSource(); + _showDialogResolves.Add(dialogId, new Tuple, DateTime>(tcs, DateTime.UtcNow)); + return tcs.Task; + } + + public bool IsDev() + { + return Core.Utilities.CoreHelpers.InDebugMode(); + } + + public bool IsSelfHost() + { + return false; + } + + public async Task CopyToClipboardAsync(string text, Dictionary options = null) + { + var clearMs = options != null && options.ContainsKey("clearMs") ? (int?)options["clearMs"] : null; + await Clipboard.SetTextAsync(text); + _messagingService.Send("copiedToClipboard", new Tuple(text, clearMs)); + } + + public async Task ReadFromClipboardAsync(Dictionary options = null) + { + return await Clipboard.GetTextAsync(); + } + + public async Task SupportsFingerprintAsync() + { + try + { + return await CrossFingerprint.Current.IsAvailableAsync(); + } + catch + { + return false; + } + } + + public async Task AuthenticateFingerprintAsync(string text = null, string fallbackText = null, + Action fallback = null) + { + try + { + if(text == null) + { + text = _deviceActionService.SupportsFaceId() ? AppResources.FaceIDDirection : + AppResources.FingerprintDirection; + } + var fingerprintRequest = new AuthenticationRequestConfiguration(text) + { + AllowAlternativeAuthentication = true, + CancelTitle = AppResources.Cancel, + FallbackTitle = fallbackText + }; + var result = await CrossFingerprint.Current.AuthenticateAsync(fingerprintRequest); + if(result.Authenticated) + { + return true; + } + else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested) + { + fallback?.Invoke(); + } + } + catch { } + return false; + } + } +} diff --git a/src/App/Services/MobileStorageService.cs b/src/App/Services/MobileStorageService.cs new file mode 100644 index 000000000..c74eb52c4 --- /dev/null +++ b/src/App/Services/MobileStorageService.cs @@ -0,0 +1,53 @@ +using Bit.Core; +using Bit.Core.Abstractions; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.App.Services +{ + public class MobileStorageService : IStorageService + { + private readonly IStorageService _preferencesStorageService; + private readonly IStorageService _liteDbStorageService; + + private readonly HashSet _preferenceStorageKeys = new HashSet + { + Constants.LockOptionKey + }; + + public MobileStorageService( + IStorageService preferenceStorageService, + IStorageService liteDbStorageService) + { + _preferencesStorageService = preferenceStorageService; + _liteDbStorageService = liteDbStorageService; + } + + public Task GetAsync(string key) + { + if(_preferenceStorageKeys.Contains(key)) + { + return _preferencesStorageService.GetAsync(key); + } + return _liteDbStorageService.GetAsync(key); + } + + public Task SaveAsync(string key, T obj) + { + if(_preferenceStorageKeys.Contains(key)) + { + return _preferencesStorageService.SaveAsync(key, obj); + } + return _liteDbStorageService.SaveAsync(key, obj); + } + + public Task RemoveAsync(string key) + { + if(_preferenceStorageKeys.Contains(key)) + { + return _preferencesStorageService.RemoveAsync(key); + } + return _liteDbStorageService.RemoveAsync(key); + } + } +} diff --git a/src/App/Services/NoopGoogleAnalyticsService.cs b/src/App/Services/NoopGoogleAnalyticsService.cs deleted file mode 100644 index 3c3362136..000000000 --- a/src/App/Services/NoopGoogleAnalyticsService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Bit.App.Abstractions; - -namespace Bit.Android.Services -{ - public class NoopGoogleAnalyticsService : IGoogleAnalyticsService - { - public void TrackAppEvent(string eventName, string label = null) - { - } - - public void TrackExtensionEvent(string eventName, string label = null) - { - } - - public void TrackAutofillExtensionEvent(string eventName, string label = null) - { - } - - public void TrackEvent(string category, string eventName, string label = null) - { - } - - public void TrackException(string message, bool fatal) - { - } - - public void TrackPage(string pageName) - { - } - - public void Dispatch(Action completionHandler = null) - { - } - - public void SetAppOptOut(bool optOut) - { - } - } -} diff --git a/src/App/Services/NoopPushNotificationListener.cs b/src/App/Services/NoopPushNotificationListenerService.cs similarity index 53% rename from src/App/Services/NoopPushNotificationListener.cs rename to src/App/Services/NoopPushNotificationListenerService.cs index 793263dfe..7bb42b424 100644 --- a/src/App/Services/NoopPushNotificationListener.cs +++ b/src/App/Services/NoopPushNotificationListenerService.cs @@ -1,16 +1,19 @@ using Newtonsoft.Json.Linq; using Bit.App.Abstractions; +using System.Threading.Tasks; namespace Bit.App.Services { - public class NoopPushNotificationListener : IPushNotificationListener + public class NoopPushNotificationListenerService : IPushNotificationListenerService { - public void OnMessage(JObject value, string deviceType) + public Task OnMessageAsync(JObject value, string deviceType) { + return Task.FromResult(0); } - public void OnRegistered(string token, string deviceType) + public Task OnRegisteredAsync(string token, string deviceType) { + return Task.FromResult(0); } public void OnUnregistered(string deviceType) diff --git a/src/App/Services/NoopPushNotificationService.cs b/src/App/Services/NoopPushNotificationService.cs index 5f9119614..edf501a73 100644 --- a/src/App/Services/NoopPushNotificationService.cs +++ b/src/App/Services/NoopPushNotificationService.cs @@ -1,17 +1,23 @@ -using Bit.App.Abstractions; +using System.Threading.Tasks; +using Bit.App.Abstractions; namespace Bit.App.Services { public class NoopPushNotificationService : IPushNotificationService { - public string Token => null; - - public void Register() + public Task GetTokenAsync() { + return Task.FromResult(null as string); } - public void Unregister() + public Task RegisterAsync() { + return Task.FromResult(0); + } + + public Task UnregisterAsync() + { + return Task.FromResult(0); } } } diff --git a/src/App/Services/PasswordGenerationService.cs b/src/App/Services/PasswordGenerationService.cs deleted file mode 100644 index 78b55f1ea..000000000 --- a/src/App/Services/PasswordGenerationService.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Bit.App.Abstractions; -using Plugin.Settings.Abstractions; -using PCLCrypto; - -namespace Bit.App.Services -{ - public class PasswordGenerationService : IPasswordGenerationService - { - private readonly ISettings _settings; - - public PasswordGenerationService(ISettings settings) - { - _settings = settings; - } - - public string GeneratePassword( - int? length = null, - bool? uppercase = null, - bool? lowercase = null, - bool? numbers = null, - bool? special = null, - bool? ambiguous = null, - int? minUppercase = null, - int? minLowercase = null, - int? minNumbers = null, - int? minSpecial = null) - { - int minUppercaseValue = minUppercase.GetValueOrDefault(0), - minLowercaseValue = minLowercase.GetValueOrDefault(0), - minNumbersValue = minNumbers.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorMinNumbers, 1)), - minSpecialValue = minSpecial.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorMinSpecial, 1)), - lengthValue = length.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorLength, 10)); - - bool uppercaseValue = uppercase.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorUppercase, true)), - lowercaseValue = lowercase.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorLowercase, true)), - numbersValue = numbers.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorNumbers, true)), - specialValue = special.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorSpecial, true)), - ambiguousValue = ambiguous.GetValueOrDefault(_settings.GetValueOrDefault(Constants.PasswordGeneratorAmbiguous, false)); - - // Sanitize - if(uppercaseValue && minUppercaseValue <= 0) - { - minUppercaseValue = 1; - } - if(lowercaseValue && minLowercaseValue <= 0) - { - minLowercaseValue = 1; - } - if(numbersValue && minNumbersValue <= 0) - { - minNumbersValue = 1; - } - if(specialValue && minSpecialValue <= 0) - { - minSpecialValue = 1; - } - - if(lengthValue < 1) - { - lengthValue = 10; - } - var minLength = minUppercaseValue + minLowercaseValue + minNumbersValue + minSpecialValue; - if(lengthValue < minLength) - { - lengthValue = minLength; - } - - var positionsBuilder = new StringBuilder(); - if(lowercaseValue && minLowercaseValue > 0) - { - for(int i = 0; i < minLowercaseValue; i++) - { - positionsBuilder.Append("l"); - } - } - if(uppercaseValue && minUppercaseValue > 0) - { - for(int i = 0; i < minUppercaseValue; i++) - { - positionsBuilder.Append("u"); - } - } - if(numbersValue && minNumbersValue > 0) - { - for(int i = 0; i < minNumbersValue; i++) - { - positionsBuilder.Append("n"); - } - } - if(specialValue && minSpecialValue > 0) - { - for(int i = 0; i < minSpecialValue; i++) - { - positionsBuilder.Append("s"); - } - } - while(positionsBuilder.Length < lengthValue) - { - positionsBuilder.Append("a"); - } - - // Shuffle - var positions = positionsBuilder.ToString().ToCharArray().OrderBy(a => Next(int.MaxValue)).ToArray(); - - // Build out other character sets - var allCharSet = string.Empty; - - var lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; - if(ambiguousValue) - { - lowercaseCharSet = string.Concat(lowercaseCharSet, "l"); - } - if(lowercaseValue) - { - allCharSet = string.Concat(allCharSet, lowercaseCharSet); - } - - var uppercaseCharSet = "ABCDEFGHIJKLMNPQRSTUVWXYZ"; - if(ambiguousValue) - { - uppercaseCharSet = string.Concat(uppercaseCharSet, "O"); - } - if(uppercaseValue) - { - allCharSet = string.Concat(allCharSet, uppercaseCharSet); - } - - var numberCharSet = "23456789"; - if(ambiguousValue) - { - numberCharSet = string.Concat(numberCharSet, "01"); - } - if(numbersValue) - { - allCharSet = string.Concat(allCharSet, numberCharSet); - } - - var specialCharSet = "!@#$%^&*"; - if(specialValue) - { - allCharSet = string.Concat(allCharSet, specialCharSet); - } - - var password = new StringBuilder(); - for(var i = 0; i < lengthValue; i++) - { - string positionChars = string.Empty; - switch(positions[i]) - { - case 'l': - positionChars = lowercaseCharSet; - break; - case 'u': - positionChars = uppercaseCharSet; - break; - case 'n': - positionChars = numberCharSet; - break; - case 's': - positionChars = specialCharSet; - break; - case 'a': - positionChars = allCharSet; - break; - } - - var randomCharIndex = Next(positionChars.Length); - password.Append(positionChars[randomCharIndex]); - } - - return password.ToString(); - } - - private int Next(int maxValue) - { - if(maxValue == 0) - { - return 0; - } - - return (int)(WinRTCrypto.CryptographicBuffer.GenerateRandomNumber() % maxValue); - } - } -} diff --git a/src/App/Services/PreferencesStorageService.cs b/src/App/Services/PreferencesStorageService.cs new file mode 100644 index 000000000..f7a54c3a3 --- /dev/null +++ b/src/App/Services/PreferencesStorageService.cs @@ -0,0 +1,134 @@ +using Bit.Core.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Threading.Tasks; + +namespace Bit.App.Services +{ + public class PreferencesStorageService : IStorageService + { + private readonly string _keyFormat = "bwPreferencesStorage:{0}"; + private readonly string _sharedName; + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public PreferencesStorageService(string sharedName) + { + _sharedName = sharedName; + } + + public Task GetAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + if(!Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) + { + return Task.FromResult(default(T)); + } + + var objType = typeof(T); + if(objType == typeof(string)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); + return Task.FromResult((T)(object)val); + } + else if(objType == typeof(bool) || objType == typeof(bool?)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(bool), _sharedName); + return Task.FromResult(ChangeType(val)); + } + else if(objType == typeof(int) || objType == typeof(int?)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(int), _sharedName); + return Task.FromResult(ChangeType(val)); + } + else if(objType == typeof(long) || objType == typeof(long?)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(long), _sharedName); + return Task.FromResult(ChangeType(val)); + } + else if(objType == typeof(double) || objType == typeof(double?)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(double), _sharedName); + return Task.FromResult(ChangeType(val)); + } + else if(objType == typeof(DateTime) || objType == typeof(DateTime?)) + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(DateTime), _sharedName); + return Task.FromResult(ChangeType(val)); + } + else + { + var val = Xamarin.Essentials.Preferences.Get(formattedKey, default(string), _sharedName); + return Task.FromResult(JsonConvert.DeserializeObject(val, _jsonSettings)); + } + } + + public Task SaveAsync(string key, T obj) + { + if(obj == null) + { + return RemoveAsync(key); + } + + var formattedKey = string.Format(_keyFormat, key); + var objType = typeof(T); + if(objType == typeof(string)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, obj as string, _sharedName); + } + else if(objType == typeof(bool) || objType == typeof(bool?)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, (obj as bool?).Value, _sharedName); + } + else if(objType == typeof(int) || objType == typeof(int?)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, (obj as int?).Value, _sharedName); + } + else if(objType == typeof(long) || objType == typeof(long?)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, (obj as long?).Value, _sharedName); + } + else if(objType == typeof(double) || objType == typeof(double?)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, (obj as double?).Value, _sharedName); + } + else if(objType == typeof(DateTime) || objType == typeof(DateTime?)) + { + Xamarin.Essentials.Preferences.Set(formattedKey, (obj as DateTime?).Value, _sharedName); + } + else + { + Xamarin.Essentials.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings), + _sharedName); + } + return Task.FromResult(0); + } + + public Task RemoveAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + if(Xamarin.Essentials.Preferences.ContainsKey(formattedKey, _sharedName)) + { + Xamarin.Essentials.Preferences.Remove(formattedKey, _sharedName); + } + return Task.FromResult(0); + } + + private static T ChangeType(object value) + { + var t = typeof(T); + if(t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if(value == null) + { + return default(T); + } + t = Nullable.GetUnderlyingType(t); + } + return (T)Convert.ChangeType(value, t); + } + } +} diff --git a/src/App/Services/PushNotificationListener.cs b/src/App/Services/PushNotificationListener.cs deleted file mode 100644 index 652bb20f8..000000000 --- a/src/App/Services/PushNotificationListener.cs +++ /dev/null @@ -1,206 +0,0 @@ -#if !FDROID -using System.Diagnostics; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Bit.App.Abstractions; -using Bit.App.Models; -using Plugin.Settings.Abstractions; -using System; -using XLabs.Ioc; -using Xamarin.Forms; - -namespace Bit.App.Services -{ - public class PushNotificationListener : IPushNotificationListener - { - private bool _showNotification; - private bool _resolved; - private ISyncService _syncService; - private IDeviceApiRepository _deviceApiRepository; - private IAuthService _authService; - private IAppIdService _appIdService; - private ISettings _settings; - - private void Resolve() - { - if(_resolved) - { - return; - } - - _syncService = Resolver.Resolve(); - _deviceApiRepository = Resolver.Resolve(); - _authService = Resolver.Resolve(); - _appIdService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - - _resolved = true; - } - - public void OnMessage(JObject value, string deviceType) - { - Resolve(); - - if(value == null) - { - return; - } - - _showNotification = false; - Debug.WriteLine("Message Arrived: {0}", JsonConvert.SerializeObject(value)); - - if(!_authService.IsAuthenticated) - { - return; - } - - PushNotificationDataPayload data = null; - if(deviceType == Device.Android) - { - data = value.ToObject(); - } - else - { - if(!value.TryGetValue("data", StringComparison.OrdinalIgnoreCase, out JToken dataToken) || - dataToken == null) - { - return; - } - data = dataToken.ToObject(); - } - - if(data?.Payload == null) - { - return; - } - - switch(data.Type) - { - case Enums.PushType.SyncCipherUpdate: - case Enums.PushType.SyncCipherCreate: - var cipherCreateUpdateMessage = JsonConvert.DeserializeObject(data.Payload); - if(cipherCreateUpdateMessage.OrganizationId == null && - cipherCreateUpdateMessage.UserId != _authService.UserId) - { - break; - } - else if(cipherCreateUpdateMessage.OrganizationId != null && - !_authService.BelongsToOrganization(cipherCreateUpdateMessage.OrganizationId)) - { - break; - } - _syncService.SyncCipherAsync(cipherCreateUpdateMessage.Id); - break; - case Enums.PushType.SyncFolderUpdate: - case Enums.PushType.SyncFolderCreate: - var folderCreateUpdateMessage = JsonConvert.DeserializeObject(data.Payload); - if(folderCreateUpdateMessage.UserId != _authService.UserId) - { - break; - } - _syncService.SyncFolderAsync(folderCreateUpdateMessage.Id); - break; - case Enums.PushType.SyncFolderDelete: - var folderDeleteMessage = JsonConvert.DeserializeObject(data.Payload); - if(folderDeleteMessage.UserId != _authService.UserId) - { - break; - } - _syncService.SyncDeleteFolderAsync(folderDeleteMessage.Id, folderDeleteMessage.RevisionDate); - break; - case Enums.PushType.SyncLoginDelete: - var loginDeleteMessage = JsonConvert.DeserializeObject(data.Payload); - if(loginDeleteMessage.OrganizationId == null && - loginDeleteMessage.UserId != _authService.UserId) - { - break; - } - else if(loginDeleteMessage.OrganizationId != null && - !_authService.BelongsToOrganization(loginDeleteMessage.OrganizationId)) - { - break; - } - _syncService.SyncDeleteCipherAsync(loginDeleteMessage.Id); - break; - case Enums.PushType.SyncCiphers: - case Enums.PushType.SyncVault: - var cipherMessage = JsonConvert.DeserializeObject(data.Payload); - if(cipherMessage.UserId != _authService.UserId) - { - break; - } - _syncService.FullSyncAsync(true); - break; - case Enums.PushType.SyncSettings: - var domainMessage = JsonConvert.DeserializeObject(data.Payload); - if(domainMessage.UserId != _authService.UserId) - { - break; - } - _syncService.SyncSettingsAsync(); - break; - case Enums.PushType.SyncOrgKeys: - var orgKeysMessage = JsonConvert.DeserializeObject(data.Payload); - if(orgKeysMessage.UserId != _authService.UserId) - { - break; - } - _syncService.SyncProfileAsync(); - break; - case Enums.PushType.LogOut: - var logOutMessage = JsonConvert.DeserializeObject(data.Payload); - if(logOutMessage.UserId == _authService.UserId) - { - _authService.LogOut(null); - } - break; - default: - break; - } - } - - public async void OnRegistered(string token, string deviceType) - { - Resolve(); - - Debug.WriteLine(string.Format("Push Notification - Device Registered - Token : {0}", token)); - - if(!_authService.IsAuthenticated) - { - return; - } - - var response = await _deviceApiRepository.PutTokenAsync(_appIdService.AppId, - new Models.Api.DeviceTokenRequest(token)); - if(response.Succeeded) - { - Debug.WriteLine("Registered device with server."); - _settings.AddOrUpdateValue(Constants.PushLastRegistrationDate, DateTime.UtcNow); - if(deviceType == Device.Android) - { - _settings.AddOrUpdateValue(Constants.PushCurrentToken, token); - } - } - else - { - Debug.WriteLine("Failed to register device."); - } - } - - public void OnUnregistered(string deviceType) - { - Debug.WriteLine("Push Notification - Device Unnregistered"); - } - - public void OnError(string message, string deviceType) - { - Debug.WriteLine(string.Format("Push notification error - {0}", message)); - } - - public bool ShouldShowNotification() - { - return _showNotification; - } - } -} -#endif diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs new file mode 100644 index 000000000..c06abc84e --- /dev/null +++ b/src/App/Services/PushNotificationListenerService.cs @@ -0,0 +1,187 @@ +#if !FDROID +using System.Diagnostics; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Bit.App.Abstractions; +using System; +using Xamarin.Forms; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core; +using Bit.Core.Models.Response; +using Bit.Core.Exceptions; + +namespace Bit.App.Services +{ + public class PushNotificationListenerService : IPushNotificationListenerService + { + private bool _showNotification; + private bool _resolved; + private IStorageService _storageService; + private ISyncService _syncService; + private IUserService _userService; + private IAppIdService _appIdService; + private IApiService _apiService; + private IMessagingService _messagingService; + + public async Task OnMessageAsync(JObject value, string deviceType) + { + Resolve(); + if(value == null) + { + return; + } + + _showNotification = false; + Debug.WriteLine("Message Arrived: {0}", JsonConvert.SerializeObject(value)); + + NotificationResponse notification = null; + if(deviceType == Device.Android) + { + notification = value.ToObject(); + } + else + { + if(!value.TryGetValue("data", StringComparison.OrdinalIgnoreCase, out JToken dataToken) || + dataToken == null) + { + return; + } + notification = dataToken.ToObject(); + } + + var appId = await _appIdService.GetAppIdAsync(); + if(notification?.Payload == null || notification.ContextId == appId) + { + return; + } + + var myUserId = await _userService.GetUserIdAsync(); + var isAuthenticated = await _userService.IsAuthenticatedAsync(); + switch(notification.Type) + { + case NotificationType.SyncCipherUpdate: + case NotificationType.SyncCipherCreate: + var cipherCreateUpdateMessage = JsonConvert.DeserializeObject( + notification.Payload); + if(isAuthenticated && cipherCreateUpdateMessage.UserId == myUserId) + { + await _syncService.SyncUpsertCipherAsync(cipherCreateUpdateMessage, + notification.Type == NotificationType.SyncCipherUpdate); + } + break; + case NotificationType.SyncFolderUpdate: + case NotificationType.SyncFolderCreate: + var folderCreateUpdateMessage = JsonConvert.DeserializeObject( + notification.Payload); + if(isAuthenticated && folderCreateUpdateMessage.UserId == myUserId) + { + await _syncService.SyncUpsertFolderAsync(folderCreateUpdateMessage, + notification.Type == NotificationType.SyncFolderUpdate); + } + break; + case NotificationType.SyncLoginDelete: + case NotificationType.SyncCipherDelete: + var loginDeleteMessage = JsonConvert.DeserializeObject( + notification.Payload); + if(isAuthenticated && loginDeleteMessage.UserId == myUserId) + { + await _syncService.SyncDeleteCipherAsync(loginDeleteMessage); + } + break; + case NotificationType.SyncFolderDelete: + var folderDeleteMessage = JsonConvert.DeserializeObject( + notification.Payload); + if(isAuthenticated && folderDeleteMessage.UserId == myUserId) + { + await _syncService.SyncDeleteFolderAsync(folderDeleteMessage); + } + break; + case NotificationType.SyncCiphers: + case NotificationType.SyncVault: + case NotificationType.SyncSettings: + if(isAuthenticated) + { + await _syncService.FullSyncAsync(false); + } + break; + case NotificationType.SyncOrgKeys: + if(isAuthenticated) + { + await _apiService.RefreshIdentityTokenAsync(); + await _syncService.FullSyncAsync(true); + } + break; + case NotificationType.LogOut: + if(isAuthenticated) + { + _messagingService.Send("logout"); + } + break; + default: + break; + } + } + + public async Task OnRegisteredAsync(string token, string deviceType) + { + Resolve(); + Debug.WriteLine(string.Format("Push Notification - Device Registered - Token : {0}", token)); + var isAuthenticated = await _userService.IsAuthenticatedAsync(); + if(!isAuthenticated) + { + return; + } + + var appId = await _appIdService.GetAppIdAsync(); + try + { + await _apiService.PutDeviceTokenAsync(appId, + new Core.Models.Request.DeviceTokenRequest { PushToken = token }); + Debug.WriteLine("Registered device with server."); + await _storageService.SaveAsync(Constants.PushLastRegistrationDateKey, DateTime.UtcNow); + if(deviceType == Device.Android) + { + await _storageService.SaveAsync(Constants.PushCurrentTokenKey, token); + } + } + catch(ApiException) + { + Debug.WriteLine("Failed to register device."); + } + } + + public void OnUnregistered(string deviceType) + { + Debug.WriteLine("Push Notification - Device Unnregistered"); + } + + public void OnError(string message, string deviceType) + { + Debug.WriteLine(string.Format("Push notification error - {0}", message)); + } + + public bool ShouldShowNotification() + { + return _showNotification; + } + + private void Resolve() + { + if(_resolved) + { + return; + } + _storageService = ServiceContainer.Resolve("storageService"); + _syncService = ServiceContainer.Resolve("syncService"); + _userService = ServiceContainer.Resolve("userService"); + _appIdService = ServiceContainer.Resolve("appIdService"); + _apiService = ServiceContainer.Resolve("apiService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _resolved = true; + } + } +} +#endif diff --git a/src/App/Services/SecureStorageService.cs b/src/App/Services/SecureStorageService.cs new file mode 100644 index 000000000..09434e021 --- /dev/null +++ b/src/App/Services/SecureStorageService.cs @@ -0,0 +1,56 @@ +using Bit.Core.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Threading.Tasks; + +namespace Bit.App.Services +{ + public class SecureStorageService : IStorageService + { + private readonly string _keyFormat = "bwSecureStorage:{0}"; + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public async Task GetAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + var val = await Xamarin.Essentials.SecureStorage.GetAsync(formattedKey); + if(typeof(T) == typeof(string)) + { + return (T)(object)val; + } + else + { + return JsonConvert.DeserializeObject(val, _jsonSettings); + } + } + + public async Task SaveAsync(string key, T obj) + { + if(obj == null) + { + await RemoveAsync(key); + return; + } + var formattedKey = string.Format(_keyFormat, key); + if(typeof(T) == typeof(string)) + { + await Xamarin.Essentials.SecureStorage.SetAsync(formattedKey, obj as string); + } + else + { + await Xamarin.Essentials.SecureStorage.SetAsync(formattedKey, + JsonConvert.SerializeObject(obj, _jsonSettings)); + } + } + + public Task RemoveAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + Xamarin.Essentials.SecureStorage.Remove(formattedKey); + return Task.FromResult(0); + } + } +} diff --git a/src/App/Services/SettingsService.cs b/src/App/Services/SettingsService.cs deleted file mode 100644 index 2352725d1..000000000 --- a/src/App/Services/SettingsService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bit.App.Abstractions; -using Plugin.Settings.Abstractions; -using System.Collections.Generic; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace Bit.App.Services -{ - public class SettingsService : ISettingsService - { - private readonly ISettingsRepository _settingsRepository; - private readonly ISettings _settings; - private readonly IAuthService _authService; - - public SettingsService( - ISettingsRepository settingsRepository, - ISettings settings, - IAuthService authService) - { - _settingsRepository = settingsRepository; - _settings = settings; - _authService = authService; - } - - public async Task>> GetEquivalentDomainsAsync() - { - var settings = await _settingsRepository.GetByIdAsync(_authService.UserId); - if(string.IsNullOrWhiteSpace(settings?.EquivalentDomains)) - { - return new List(); - } - - return JsonConvert.DeserializeObject>>(settings.EquivalentDomains); - } - } -} diff --git a/src/App/Services/SyncService.cs b/src/App/Services/SyncService.cs deleted file mode 100644 index f44054dac..000000000 --- a/src/App/Services/SyncService.cs +++ /dev/null @@ -1,595 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Models.Data; -using Plugin.Settings.Abstractions; -using Bit.App.Models.Api; -using System.Collections.Generic; -using Xamarin.Forms; -using Newtonsoft.Json; -using Bit.App.Models; - -namespace Bit.App.Services -{ - public class SyncService : ISyncService - { - private readonly ICipherApiRepository _cipherApiRepository; - private readonly IFolderApiRepository _folderApiRepository; - private readonly IAccountsApiRepository _accountsApiRepository; - private readonly ISettingsApiRepository _settingsApiRepository; - private readonly ISyncApiRepository _syncApiRepository; - private readonly IFolderRepository _folderRepository; - private readonly ICollectionRepository _collectionRepository; - private readonly ICipherCollectionRepository _cipherCollectionRepository; - private readonly ICipherService _cipherService; - private readonly IAttachmentRepository _attachmentRepository; - private readonly ISettingsRepository _settingsRepository; - private readonly IAuthService _authService; - private readonly ICryptoService _cryptoService; - private readonly ISettings _settings; - private readonly IAppSettingsService _appSettingsService; - - public SyncService( - ICipherApiRepository cipherApiRepository, - IFolderApiRepository folderApiRepository, - IAccountsApiRepository accountsApiRepository, - ISettingsApiRepository settingsApiRepository, - ISyncApiRepository syncApiRepository, - IFolderRepository folderRepository, - ICollectionRepository collectionRepository, - ICipherCollectionRepository cipherCollectionRepository, - ICipherService cipherService, - IAttachmentRepository attachmentRepository, - ISettingsRepository settingsRepository, - IAuthService authService, - ICryptoService cryptoService, - ISettings settings, - IAppSettingsService appSettingsService) - { - _cipherApiRepository = cipherApiRepository; - _folderApiRepository = folderApiRepository; - _accountsApiRepository = accountsApiRepository; - _settingsApiRepository = settingsApiRepository; - _syncApiRepository = syncApiRepository; - _folderRepository = folderRepository; - _collectionRepository = collectionRepository; - _cipherCollectionRepository = cipherCollectionRepository; - _cipherService = cipherService; - _attachmentRepository = attachmentRepository; - _settingsRepository = settingsRepository; - _authService = authService; - _cryptoService = cryptoService; - _settings = settings; - _appSettingsService = appSettingsService; - } - - public bool SyncInProgress { get; private set; } - - public async Task SyncCipherAsync(string id) - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - var cipher = await _cipherApiRepository.GetByIdAsync(id).ConfigureAwait(false); - if(!CheckSuccess(cipher)) - { - return false; - } - - try - { - var existingCipher = await _cipherService.GetByIdAsync(cipher.Result.Id); - var cipherData = new CipherData(cipher.Result, _authService.UserId); - await _cipherService.UpsertDataAsync(cipherData, true, existingCipher == null).ConfigureAwait(false); - - var localAttachments = (await _attachmentRepository.GetAllByCipherIdAsync(cipherData.Id) - .ConfigureAwait(false)); - - if(cipher.Result.Attachments != null) - { - var attachmentData = cipher.Result.Attachments.Select(a => new AttachmentData(a, cipherData.Id)); - await _cipherService.UpsertAttachmentDataAsync(attachmentData).ConfigureAwait(false); - } - - if(localAttachments != null) - { - foreach(var attachment in localAttachments - .Where(a => !cipher.Result.Attachments.Any(sa => sa.Id == a.Id))) - { - try - { - await _cipherService.DeleteAttachmentDataAsync(attachment.Id).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - } - catch(SQLite.SQLiteException) - { - SyncCompleted(false); - return false; - } - - SyncCompleted(true); - return true; - } - - public async Task SyncFolderAsync(string id) - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - var folder = await _folderApiRepository.GetByIdAsync(id).ConfigureAwait(false); - if(!CheckSuccess(folder)) - { - return false; - } - - try - { - var folderData = new FolderData(folder.Result, _authService.UserId); - await _folderRepository.UpsertAsync(folderData).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) - { - SyncCompleted(false); - return false; - } - - SyncCompleted(true); - return true; - } - - public async Task SyncDeleteFolderAsync(string id, DateTime revisionDate) - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - try - { - await _folderRepository.DeleteWithCipherUpdateAsync(id, revisionDate).ConfigureAwait(false); - SyncCompleted(true); - return true; - } - catch(SQLite.SQLiteException) - { - SyncCompleted(false); - return false; - } - } - - public async Task SyncDeleteCipherAsync(string id) - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - try - { - await _cipherService.DeleteDataAsync(id, true).ConfigureAwait(false); - SyncCompleted(true); - return true; - } - catch(SQLite.SQLiteException) - { - SyncCompleted(false); - return false; - } - } - - public async Task SyncSettingsAsync() - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - var domains = await _settingsApiRepository.GetDomains(false).ConfigureAwait(false); - if(!CheckSuccess(domains)) - { - return false; - } - - await SyncDomainsAsync(domains.Result); - - SyncCompleted(true); - return true; - } - - public async Task SyncProfileAsync() - { - if(!_authService.IsAuthenticated) - { - return false; - } - - SyncStarted(); - - var profile = await _accountsApiRepository.GetProfileAsync().ConfigureAwait(false); - if(!CheckSuccess(profile, !string.IsNullOrWhiteSpace(_appSettingsService.SecurityStamp) && - _appSettingsService.SecurityStamp != profile.Result.SecurityStamp)) - { - return false; - } - - await SyncProfileKeysAsync(profile.Result); - - SyncCompleted(true); - return true; - } - - public async Task FullSyncAsync(TimeSpan syncThreshold, bool forceSync = false) - { - var lastSync = _settings.GetValueOrDefault(Constants.LastSync, DateTime.MinValue); - if(DateTime.UtcNow - lastSync < syncThreshold) - { - return false; - } - - return await FullSyncAsync(forceSync).ConfigureAwait(false); - } - - public async Task FullSyncAsync(bool forceSync = false) - { - if(!_authService.IsAuthenticated) - { - return false; - } - - if(!forceSync && !(await NeedsToSyncAsync())) - { - _settings.AddOrUpdateValue(Constants.LastSync, DateTime.UtcNow); - return false; - } - - SyncStarted(); - - var now = DateTime.UtcNow; - - var syncResponse = await _syncApiRepository.Get(); - if(!CheckSuccess(syncResponse, - !string.IsNullOrWhiteSpace(_appSettingsService.SecurityStamp) && - syncResponse.Result?.Profile != null && - _appSettingsService.SecurityStamp != syncResponse.Result.Profile.SecurityStamp)) - { - return false; - } - - var ciphersDict = syncResponse.Result.Ciphers.ToDictionary(s => s.Id); - var foldersDict = syncResponse.Result.Folders.ToDictionary(f => f.Id); - var collectionsDict = syncResponse.Result.Collections?.ToDictionary(c => c.Id); - - var cipherTask = SyncCiphersAsync(ciphersDict); - var folderTask = SyncFoldersAsync(foldersDict); - var collectionsTask = SyncCollectionsAsync(collectionsDict); - var domainsTask = SyncDomainsAsync(syncResponse.Result.Domains); - var profileTask = SyncProfileKeysAsync(syncResponse.Result.Profile); - await Task.WhenAll(cipherTask, folderTask, collectionsTask, domainsTask, profileTask).ConfigureAwait(false); - - if(folderTask.Exception != null || cipherTask.Exception != null || collectionsTask.Exception != null || - domainsTask.Exception != null || profileTask.Exception != null) - { - SyncCompleted(false); - MessagingCenter.Send(Application.Current, "FullSyncCompleted", false); - return false; - } - - _settings.AddOrUpdateValue(Constants.LastSync, now); - SyncCompleted(true); - MessagingCenter.Send(Application.Current, "FullSyncCompleted", true); - return true; - } - - private async Task NeedsToSyncAsync() - { - if(!_settings.Contains(Constants.LastSync)) - { - return true; - } - var lastSync = _settings.GetValueOrDefault(Constants.LastSync, DateTime.MinValue); - - var accountRevisionDate = await _accountsApiRepository.GetAccountRevisionDateAsync(); - if(accountRevisionDate.Succeeded && accountRevisionDate.Result.HasValue && - accountRevisionDate.Result.Value > lastSync) - { - return true; - } - - if(Application.Current != null && (accountRevisionDate.StatusCode == System.Net.HttpStatusCode.Forbidden - || accountRevisionDate.StatusCode == System.Net.HttpStatusCode.Unauthorized)) - { - _authService.LogOut(); - } - - return false; - } - - private async Task SyncFoldersAsync(IDictionary serverFolders) - { - if(!_authService.IsAuthenticated) - { - return; - } - - var localFolders = (await _folderRepository.GetAllByUserIdAsync(_authService.UserId) - .ConfigureAwait(false)) - .GroupBy(f => f.Id) - .Select(f => f.First()) - .ToDictionary(f => f.Id); - - foreach(var serverFolder in serverFolders) - { - if(!_authService.IsAuthenticated) - { - return; - } - - try - { - var data = new FolderData(serverFolder.Value, _authService.UserId); - await _folderRepository.UpsertAsync(data).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - - foreach(var folder in localFolders.Where(localFolder => !serverFolders.ContainsKey(localFolder.Key))) - { - try - { - await _folderRepository.DeleteAsync(folder.Value.Id).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - - private async Task SyncCollectionsAsync(IDictionary serverCollections) - { - if(!_authService.IsAuthenticated) - { - return; - } - - var localCollections = (await _collectionRepository.GetAllByUserIdAsync(_authService.UserId) - .ConfigureAwait(false)) - .GroupBy(f => f.Id) - .Select(f => f.First()) - .ToDictionary(f => f.Id); - - if(serverCollections != null) - { - foreach(var serverCollection in serverCollections) - { - if(!_authService.IsAuthenticated) - { - return; - } - - try - { - var data = new CollectionData(serverCollection.Value, _authService.UserId); - await _collectionRepository.UpsertAsync(data).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - - foreach(var collection in localCollections.Where(lc => !serverCollections.ContainsKey(lc.Key))) - { - try - { - await _collectionRepository.DeleteAsync(collection.Value.Id).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - - private async Task SyncCiphersAsync(IDictionary serverCiphers) - { - if(!_authService.IsAuthenticated) - { - return; - } - - var localCiphers = (await _cipherService.GetAllAsync() - .ConfigureAwait(false)) - .GroupBy(s => s.Id) - .Select(s => s.First()) - .ToDictionary(s => s.Id); - - var localAttachments = (await _attachmentRepository.GetAllByUserIdAsync(_authService.UserId) - .ConfigureAwait(false)) - .GroupBy(a => a.LoginId) - .ToDictionary(g => g.Key); - - var cipherCollections = new List(); - foreach(var serverCipher in serverCiphers) - { - if(!_authService.IsAuthenticated) - { - return; - } - - var collectionForThisCipher = serverCipher.Value.CollectionIds?.Select(cid => new CipherCollectionData - { - CipherId = serverCipher.Value.Id, - CollectionId = cid, - UserId = _authService.UserId - }).ToList(); - - if(collectionForThisCipher != null && collectionForThisCipher.Any()) - { - cipherCollections.AddRange(collectionForThisCipher); - } - - try - { - var localCipher = localCiphers.ContainsKey(serverCipher.Value.Id) ? - localCiphers[serverCipher.Value.Id] : null; - - var data = new CipherData(serverCipher.Value, _authService.UserId); - await _cipherService.UpsertDataAsync(data, false, false).ConfigureAwait(false); - - if(serverCipher.Value.Attachments != null) - { - var attachmentData = serverCipher.Value.Attachments.Select(a => new AttachmentData(a, data.Id)); - await _cipherService.UpsertAttachmentDataAsync(attachmentData).ConfigureAwait(false); - } - - if(localCipher != null && localAttachments != null && localAttachments.ContainsKey(localCipher.Id)) - { - foreach(var attachment in localAttachments[localCipher.Id] - .Where(a => !serverCipher.Value.Attachments.Any(sa => sa.Id == a.Id))) - { - try - { - await _cipherService.DeleteAttachmentDataAsync(attachment.Id).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - } - catch(SQLite.SQLiteException) { } - } - - foreach(var cipher in localCiphers.Where(local => !serverCiphers.ContainsKey(local.Key))) - { - try - { - await _cipherService.DeleteDataAsync(cipher.Value.Id, false).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - - try - { - await _cipherCollectionRepository.DeleteByUserIdAsync(_authService.UserId).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - - foreach(var cipherCollection in cipherCollections) - { - try - { - await _cipherCollectionRepository.InsertAsync(cipherCollection).ConfigureAwait(false); - } - catch(SQLite.SQLiteException) { } - } - } - - private async Task SyncDomainsAsync(DomainsResponse serverDomains) - { - if(serverDomains == null) - { - return; - } - - var eqDomains = new List>(); - if(serverDomains.EquivalentDomains != null) - { - eqDomains.AddRange(serverDomains.EquivalentDomains); - } - - if(serverDomains.GlobalEquivalentDomains != null) - { - eqDomains.AddRange(serverDomains.GlobalEquivalentDomains.Select(d => d.Domains)); - } - - try - { - await _settingsRepository.UpsertAsync(new SettingsData - { - Id = _authService.UserId, - EquivalentDomains = JsonConvert.SerializeObject(eqDomains) - }); - } - catch(SQLite.SQLiteException) { } - } - - private Task SyncProfileKeysAsync(ProfileResponse profile) - { - if(profile == null) - { - return Task.FromResult(0); - } - - if(!string.IsNullOrWhiteSpace(profile.Key)) - { - _cryptoService.SetEncKey(new CipherString(profile.Key)); - } - - if(!string.IsNullOrWhiteSpace(profile.PrivateKey)) - { - _cryptoService.SetPrivateKey(new CipherString(profile.PrivateKey)); - } - - if(!string.IsNullOrWhiteSpace(profile.SecurityStamp)) - { - _appSettingsService.SecurityStamp = profile.SecurityStamp; - } - - _cryptoService.SetOrgKeys(profile); - _appSettingsService.OrganizationGivesPremium = - profile.Organizations?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; - return Task.FromResult(0); - } - - private void SyncStarted() - { - if(Application.Current == null) - { - return; - } - - SyncInProgress = true; - MessagingCenter.Send(Application.Current, "SyncStarted"); - } - - private void SyncCompleted(bool successfully) - { - if(Application.Current == null) - { - return; - } - - SyncInProgress = false; - MessagingCenter.Send(Application.Current, "SyncCompleted", successfully); - } - - private bool CheckSuccess(ApiResult result, bool logout = false) - { - if(!result.Succeeded || logout) - { - SyncCompleted(false); - - if(Application.Current != null && (logout || - result.StatusCode == System.Net.HttpStatusCode.Forbidden || - result.StatusCode == System.Net.HttpStatusCode.Unauthorized)) - { - _authService.LogOut(); - } - - return false; - } - - return true; - } - } -} diff --git a/src/App/Services/TokenService.cs b/src/App/Services/TokenService.cs deleted file mode 100644 index ced8bc4b7..000000000 --- a/src/App/Services/TokenService.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using Bit.App.Abstractions; -using System.Text; -using Newtonsoft.Json.Linq; -using Bit.App.Utilities; - -namespace Bit.App.Services -{ - public class TokenService : ITokenService - { - private const string TokenKey = "accessToken"; - private const string RefreshTokenKey = "refreshToken"; - private const string TwoFactorTokenKeyFormat = "twoFactorToken_{0}"; - - private readonly ISecureStorageService _secureStorage; - - private string _token; - private JObject _decodedToken; - private string _refreshToken; - - public TokenService(ISecureStorageService secureStorage) - { - _secureStorage = secureStorage; - } - - public string Token - { - get - { - if(_token != null) - { - return _token; - } - - var tokenBytes = _secureStorage.Retrieve(TokenKey); - if(tokenBytes == null) - { - return null; - } - - _token = Encoding.UTF8.GetString(tokenBytes, 0, tokenBytes.Length); - return _token; - } - set - { - if(value != null) - { - var tokenBytes = Encoding.UTF8.GetBytes(value); - _secureStorage.Store(TokenKey, tokenBytes); - } - else - { - _secureStorage.Delete(TokenKey); - RefreshToken = null; - } - - _decodedToken = null; - _token = value; - } - } - - public DateTime TokenExpiration - { - get - { - var decoded = DecodeToken(); - if(decoded?["exp"] == null) - { - throw new InvalidOperationException("No exp in token."); - } - - return Helpers.Epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value())); - } - } - - public string TokenIssuer - { - get - { - var decoded = DecodeToken(); - if(decoded?["iss"] == null) - { - throw new InvalidOperationException("No issuer in token."); - } - - return decoded?["iss"].Value(); - } - } - - public bool TokenExpired => DateTime.UtcNow < TokenExpiration; - public TimeSpan TokenTimeRemaining => TokenExpiration - DateTime.UtcNow; - public bool TokenNeedsRefresh => TokenTimeRemaining.TotalMinutes < 5; - public string TokenUserId => DecodeToken()?["sub"]?.Value(); - public string TokenEmail => DecodeToken()?["email"]?.Value(); - public string TokenName => DecodeToken()?["name"]?.Value(); - public bool TokenPremium => DecodeToken()?["premium"]?.Value() ?? false; - - public string RefreshToken - { - get - { - if(_refreshToken != null) - { - return _refreshToken; - } - - var tokenBytes = _secureStorage.Retrieve(RefreshTokenKey); - if(tokenBytes == null) - { - return null; - } - - _refreshToken = Encoding.UTF8.GetString(tokenBytes, 0, tokenBytes.Length); - return _refreshToken; - } - set - { - if(value != null) - { - var tokenBytes = Encoding.UTF8.GetBytes(value); - _secureStorage.Store(RefreshTokenKey, tokenBytes); - } - else - { - _secureStorage.Delete(RefreshTokenKey); - } - - _refreshToken = value; - } - } - - public string GetTwoFactorToken(string email) - { - var emailEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(email)); - var tokenBytes = _secureStorage.Retrieve(string.Format(TwoFactorTokenKeyFormat, emailEncoded)); - if(tokenBytes == null) - { - return null; - } - - return Encoding.UTF8.GetString(tokenBytes, 0, tokenBytes.Length); - } - - public void SetTwoFactorToken(string email, string token) - { - var emailEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(email)); - var key = string.Format(TwoFactorTokenKeyFormat, emailEncoded); - if(token != null) - { - var tokenBytes = Encoding.UTF8.GetBytes(token); - _secureStorage.Store(key, tokenBytes); - } - else if(_secureStorage.Contains(key)) - { - _secureStorage.Delete(key); - } - } - - public JObject DecodeToken() - { - if(_decodedToken != null) - { - return _decodedToken; - } - - if(Token == null) - { - throw new InvalidOperationException($"{nameof(Token)} not found."); - } - - var parts = Token.Split('.'); - if(parts.Length != 3) - { - throw new InvalidOperationException($"{nameof(Token)} must have 3 parts"); - } - - var decodedBytes = Base64UrlDecode(parts[1]); - if(decodedBytes == null || decodedBytes.Length < 1) - { - throw new InvalidOperationException($"{nameof(Token)} must have 3 parts"); - } - - _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length)); - return _decodedToken; - } - - private static byte[] Base64UrlDecode(string input) - { - var output = input; - // 62nd char of encoding - output = output.Replace('-', '+'); - // 63rd char of encoding - output = output.Replace('_', '/'); - // Pad with trailing '='s - switch(output.Length % 4) - { - case 0: - // No pad chars in this case - break; - case 2: - // Two pad chars - output += "=="; break; - case 3: - // One pad char - output += "="; break; - default: - throw new InvalidOperationException("Illegal base64url string!"); - } - - // Standard base64 decoder - return Convert.FromBase64String(output); - } - } -} diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml new file mode 100644 index 000000000..0410b0174 --- /dev/null +++ b/src/App/Styles/Android.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Styles/Android.xaml.cs b/src/App/Styles/Android.xaml.cs new file mode 100644 index 000000000..166079cef --- /dev/null +++ b/src/App/Styles/Android.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class Android : ResourceDictionary + { + public Android() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml new file mode 100644 index 000000000..93401d14b --- /dev/null +++ b/src/App/Styles/Base.xaml @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Styles/Base.xaml.cs b/src/App/Styles/Base.xaml.cs new file mode 100644 index 000000000..2e74ee5ea --- /dev/null +++ b/src/App/Styles/Base.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class Base : ResourceDictionary + { + public Base() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Styles/Dark.xaml b/src/App/Styles/Dark.xaml new file mode 100644 index 000000000..bf8769112 --- /dev/null +++ b/src/App/Styles/Dark.xaml @@ -0,0 +1,34 @@ + + + #5DAA00 + #3c8dbc + #dd4b39 + #00a65a + #555555 + #bf7e16 + #777777 + #007fde + #c40800 + + #dddddd + #c7c7cd + + #f0f0f0 + #3c8dbc + + #ffffff + #ffffff + #707070 + + #f0f0f0 + #3c8dbc + + #ffffff + #b5b5b5 + #3c8dbc + + #3c8dbc + #3883af + diff --git a/src/App/Styles/Dark.xaml.cs b/src/App/Styles/Dark.xaml.cs new file mode 100644 index 000000000..d84c47e4f --- /dev/null +++ b/src/App/Styles/Dark.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class Dark : ResourceDictionary + { + public Dark() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/App/Styles/Light.xaml b/src/App/Styles/Light.xaml new file mode 100644 index 000000000..ed5b1c644 --- /dev/null +++ b/src/App/Styles/Light.xaml @@ -0,0 +1,34 @@ + + + #000000 + #3c8dbc + #dd4b39 + #00a65a + #555555 + #bf7e16 + #777777 + #007fde + #c40800 + + #dddddd + #c7c7cd + + #dddddd + #3c8dbc + + #ffffff + #ffffff + #c0dbeb + + #f0f0f0 + #3c8dbc + + #ffffff + #b5b5b5 + #dddddd + + #3c8dbc + #3883af + diff --git a/src/App/Styles/Light.xaml.cs b/src/App/Styles/Light.xaml.cs new file mode 100644 index 000000000..99a500c97 --- /dev/null +++ b/src/App/Styles/Light.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class Light : ResourceDictionary + { + public Light() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/App/Styles/Variables.xaml b/src/App/Styles/Variables.xaml new file mode 100644 index 000000000..49b463edf --- /dev/null +++ b/src/App/Styles/Variables.xaml @@ -0,0 +1,6 @@ + + + + diff --git a/src/App/Styles/Variables.xaml.cs b/src/App/Styles/Variables.xaml.cs new file mode 100644 index 000000000..214142bde --- /dev/null +++ b/src/App/Styles/Variables.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class Variables : ResourceDictionary + { + public Variables() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/App/Styles/iOS.xaml b/src/App/Styles/iOS.xaml new file mode 100644 index 000000000..721b31b22 --- /dev/null +++ b/src/App/Styles/iOS.xaml @@ -0,0 +1,6 @@ + + + + diff --git a/src/App/Styles/iOS.xaml.cs b/src/App/Styles/iOS.xaml.cs new file mode 100644 index 000000000..72d94ad44 --- /dev/null +++ b/src/App/Styles/iOS.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class iOS : ResourceDictionary + { + public iOS() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/App/Utilities/ApiHttpClient.cs b/src/App/Utilities/ApiHttpClient.cs deleted file mode 100644 index bf787f252..000000000 --- a/src/App/Utilities/ApiHttpClient.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Net.Http; -using System; -using System.Net.Http.Headers; -using XLabs.Ioc; -using Bit.App.Abstractions; - -namespace Bit.App -{ - public class ApiHttpClient : HttpClient - { - public ApiHttpClient() - { - Init(); - } - - public ApiHttpClient(HttpMessageHandler handler) - : base(handler) - { - Init(); - } - - private void Init() - { - DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - var appSettings = Resolver.Resolve(); - if(!string.IsNullOrWhiteSpace(appSettings.BaseUrl)) - { - BaseAddress = new Uri($"{appSettings.BaseUrl}/api"); - } - else if(!string.IsNullOrWhiteSpace(appSettings.ApiUrl)) - { - BaseAddress = new Uri($"{appSettings.ApiUrl}"); - } - else - { - //BaseAddress = new Uri("http://169.254.80.80:4000"); // Desktop from VS Android Emulator - //BaseAddress = new Uri("http://192.168.1.3:4000"); // Desktop - //BaseAddress = new Uri("https://preview-api.bitwarden.com"); // Preview - BaseAddress = new Uri("https://api.bitwarden.com"); // Production - } - } - } -} diff --git a/src/App/Utilities/Colors.cs b/src/App/Utilities/Colors.cs deleted file mode 100644 index fcca4b00b..000000000 --- a/src/App/Utilities/Colors.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xamarin.Forms; - -namespace Bit.App.Utilities -{ - public static class Colors - { - public static Color Primary = Color.FromHex("3c8dbc"); - } -} diff --git a/src/App/Utilities/Crypto.cs b/src/App/Utilities/Crypto.cs deleted file mode 100644 index 81dc30fd5..000000000 --- a/src/App/Utilities/Crypto.cs +++ /dev/null @@ -1,246 +0,0 @@ -using Bit.App.Enums; -using Bit.App.Models; -using PCLCrypto; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Bit.App.Utilities -{ - public static class Crypto - { - private static string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; - - 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 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(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 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://github.com/mirthas/totp-net/blob/master/TOTP/Totp.cs - public static string Totp(string key) - { - var otpParams = new OtpAuth(key); - var b32Key = Base32.FromBase32(otpParams.Secret); - if(b32Key == null || b32Key.Length == 0) - { - return null; - } - - var now = Helpers.EpocUtcNow() / 1000; - var sec = now / otpParams.Period; - - var secBytes = BitConverter.GetBytes(sec); - if(BitConverter.IsLittleEndian) - { - Array.Reverse(secBytes, 0, secBytes.Length); - } - - var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(otpParams.Algorithm); - var hasher = algorithm.CreateHash(b32Key); - hasher.Append(secBytes); - var hash = hasher.GetValueAndReset(); - - var offset = (hash[hash.Length - 1] & 0xf); - var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | - ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); - - string otp = string.Empty; - if(otpParams.Steam) - { - var fullCode = binary & 0x7fffffff; - for(var i = 0; i < otpParams.Digits; i++) - { - otp += SteamChars[fullCode % SteamChars.Length]; - fullCode = (int)Math.Truncate(fullCode / (double)SteamChars.Length); - } - } - else - { - var rawOtp = binary % (int)Math.Pow(10, otpParams.Digits); - otp = rawOtp.ToString().PadLeft(otpParams.Digits, '0'); - } - return otp; - } - - // 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; - } - } -} diff --git a/src/App/Utilities/DateTimeConverter.cs b/src/App/Utilities/DateTimeConverter.cs new file mode 100644 index 000000000..6022caf2d --- /dev/null +++ b/src/App/Utilities/DateTimeConverter.cs @@ -0,0 +1,29 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public class DateTimeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + if(targetType != typeof(string)) + { + throw new InvalidOperationException("The target must be a string."); + } + if(value == null) + { + return string.Empty; + } + var d = ((DateTime)value).ToLocalTime(); + return string.Format("{0} {1}", d.ToShortDateString(), d.ToShortTimeString()); + } + + public object ConvertBack(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/App/Utilities/Extentions.cs b/src/App/Utilities/Extentions.cs deleted file mode 100644 index 5ea3c317f..000000000 --- a/src/App/Utilities/Extentions.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Bit.App.Models; -using Xamarin.Forms; -using XLabs.Ioc; -using System.Threading.Tasks; -using Bit.App.Controls; - -namespace Bit.App -{ - public static class Extentions - { - public static CipherString Encrypt(this string s, string orgId = null) - { - if(s == null) - { - throw new ArgumentNullException(nameof(s)); - } - - var cryptoService = Resolver.Resolve(); - - if(!string.IsNullOrWhiteSpace(orgId)) - { - return cryptoService.Encrypt(s, cryptoService.GetOrgKey(orgId)); - } - - return cryptoService.Encrypt(s); - } - - public static bool IsPortrait(this Page page) - { - return page.Width < page.Height; - } - - public static bool IsLandscape(this Page page) - { - return !page.IsPortrait(); - } - - public static void FocusWithDelay(this View view, int delay = 1000, bool forceDelay = false) - { - if(Device.RuntimePlatform == Device.Android || forceDelay) - { - Task.Run(async () => - { - await Task.Delay(delay); - Device.BeginInvokeOnMainThread(() => view.Focus()); - }); - } - else - { - view.Focus(); - } - } - - public static async Task PushForDeviceAsync(this INavigation navigation, Page page) - { - if (Device.RuntimePlatform != Device.UWP) - { - await navigation.PushModalAsync(new ExtendedNavigationPage(page), true); - } - else - { - await navigation.PushAsync(page, true); - } - } - - public static async Task PopForDeviceAsync(this INavigation navigation) - { - if(navigation.ModalStack.Count < 1) - { - if (navigation.NavigationStack.Count > 0 && Device.RuntimePlatform == Device.UWP) - { - await navigation.PopAsync(); - } - return; - } - - await navigation.PopModalAsync(true); - } - - public static void AdjustMarginsForDevice(this View view) - { - if(Device.RuntimePlatform == Device.Android) - { - var deviceInfo = Resolver.Resolve(); - if(deviceInfo.Version < 21) - { - view.Margin = new Thickness(-12, -5, -12, -6); - } - else if(deviceInfo.Version == 21) - { - view.Margin = new Thickness(-4, -2, -4, -11); - } - else - { - view.Margin = new Thickness(-4, -7, -4, -11); - } - } - } - - public static void AdjustPaddingForDevice(this Layout view) - { - if(Device.RuntimePlatform == Device.Android) - { - var deviceInfo = Resolver.Resolve(); - if(deviceInfo.Scale == 1) // mdpi - { - view.Padding = new Thickness(22, view.Padding.Top, 22, view.Padding.Bottom); - } - else if(deviceInfo.Scale < 2) // hdpi - { - view.Padding = new Thickness(19, view.Padding.Top, 19, view.Padding.Bottom); - } - else if(deviceInfo.Scale < 3) // xhdpi - { - view.Padding = new Thickness(17, view.Padding.Top, 17, view.Padding.Bottom); - } - else // xxhdpi and xxxhdpi - { - view.Padding = new Thickness(15, view.Padding.Top, 15, view.Padding.Bottom); - } - } - } - - public static bool LastActionWasRecent(this DateTime? lastAction, int milliseconds = 1000) - { - if(lastAction.HasValue && (DateTime.UtcNow - lastAction.Value).TotalMilliseconds < milliseconds) - { - System.Diagnostics.Debug.WriteLine("Last action occurred recently."); - return true; - } - - return false; - } - } -} diff --git a/src/App/Utilities/Helpers.cs b/src/App/Utilities/Helpers.cs deleted file mode 100644 index b3061c644..000000000 --- a/src/App/Utilities/Helpers.cs +++ /dev/null @@ -1,647 +0,0 @@ -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Enums; -using Bit.App.Models; -using Bit.App.Models.Page; -using Bit.App.Pages; -using Bit.App.Resources; -using Plugin.Settings.Abstractions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xamarin.Forms; -using XLabs.Ioc; - -namespace Bit.App.Utilities -{ - public static class Helpers - { - public static readonly DateTime Epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - public static IDictionary UriMatchOptionsMap = new Dictionary - { - [UriMatchType.Domain] = AppResources.BaseDomain, - [UriMatchType.Host] = AppResources.Host, - [UriMatchType.StartsWith] = AppResources.StartsWith, - [UriMatchType.RegularExpression] = AppResources.RegEx, - [UriMatchType.Exact] = AppResources.Exact, - [UriMatchType.Never] = AppResources.Never - }; - - public static long EpocUtcNow() - { - return (long)(DateTime.UtcNow - Epoc).TotalMilliseconds; - } - - public static T OnPlatform(T iOS = default(T), T Android = default(T), - T WinPhone = default(T), T Windows = default(T), string platform = null) - { - if(platform == null) - { - platform = Device.RuntimePlatform; - } - - switch(platform) - { - case Device.iOS: - return iOS; - case Device.Android: - return Android; - case Device.UWP: - return Windows; - default: - throw new Exception("Unsupported platform."); - } - } - - public static bool InDebugMode() - { -#if DEBUG - return true; -#else - return false; -#endif - } - - public static bool PerformUpdateTasks(ISettings settings, - IAppInfoService appInfoService, IDatabaseService databaseService, ISyncService syncService) - { - var lastBuild = settings.GetValueOrDefault(Constants.LastBuildKey, null); - if(InDebugMode() || lastBuild == null || lastBuild != appInfoService.Build) - { - settings.AddOrUpdateValue(Constants.LastBuildKey, appInfoService.Build); - databaseService.CreateTables(); - var task = Task.Run(async () => await syncService.FullSyncAsync(true)); - return true; - } - - return false; - } - - public static string GetEmptyTableSectionTitle() - { - if(Device.RuntimePlatform == Device.iOS) - { - return string.Empty; - } - - return " "; - } - - public static string ToolbarImage(string image) - { - if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.Android) - { - return null; - } - - return image; - } - - public static async void CipherMoreClickedAsync(Page page, VaultListPageModel.Cipher cipher, bool autofill) - { - var buttons = new List { AppResources.View, AppResources.Edit }; - - if(cipher.Type == CipherType.Login) - { - if(!string.IsNullOrWhiteSpace(cipher.LoginPassword.Value)) - { - buttons.Add(AppResources.CopyPassword); - } - if(!string.IsNullOrWhiteSpace(cipher.LoginUsername)) - { - buttons.Add(AppResources.CopyUsername); - } - if(!autofill && !string.IsNullOrWhiteSpace(cipher.LoginUri) && (cipher.LoginUri.StartsWith("http://") - || cipher.LoginUri.StartsWith("https://"))) - { - buttons.Add(AppResources.GoToWebsite); - } - } - else if(cipher.Type == CipherType.Card) - { - if(!string.IsNullOrWhiteSpace(cipher.CardNumber)) - { - buttons.Add(AppResources.CopyNumber); - } - if(!string.IsNullOrWhiteSpace(cipher.CardCode.Value)) - { - buttons.Add(AppResources.CopySecurityCode); - } - } - - var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, buttons.ToArray()); - - if(selection == AppResources.View) - { - var p = new VaultViewCipherPage(cipher.Type, cipher.Id); - await page.Navigation.PushForDeviceAsync(p); - } - else if(selection == AppResources.Edit) - { - var p = new VaultEditCipherPage(cipher.Id); - await page.Navigation.PushForDeviceAsync(p); - } - else if(selection == AppResources.CopyPassword) - { - CipherCopy(cipher.LoginPassword.Value, AppResources.Password); - } - else if(selection == AppResources.CopyUsername) - { - CipherCopy(cipher.LoginUsername, AppResources.Username); - } - else if(selection == AppResources.GoToWebsite) - { - Device.OpenUri(new Uri(cipher.LoginUri)); - } - else if(selection == AppResources.CopyNumber) - { - CipherCopy(cipher.CardNumber, AppResources.Number); - } - else if(selection == AppResources.CopySecurityCode) - { - CipherCopy(cipher.CardCode.Value, AppResources.SecurityCode); - } - } - - public static void CipherCopy(string copyText, string alertLabel) - { - var daService = Resolver.Resolve(); - daService.CopyToClipboard(copyText); - daService.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); - } - - public static async void AddCipher(Page page, string folderId) - { - var type = await page.DisplayActionSheet(AppResources.SelectTypeAdd, AppResources.Cancel, null, - AppResources.TypeLogin, AppResources.TypeCard, AppResources.TypeIdentity, AppResources.TypeSecureNote); - - var selectedType = CipherType.SecureNote; - if(type == null || type == AppResources.Cancel) - { - return; - } - else if(type == AppResources.TypeLogin) - { - selectedType = CipherType.Login; - } - else if(type == AppResources.TypeCard) - { - selectedType = CipherType.Card; - } - else if(type == AppResources.TypeIdentity) - { - selectedType = CipherType.Identity; - } - else if(type == AppResources.TypeSecureNote) - { - selectedType = CipherType.SecureNote; - } - else - { - return; - } - - var addPage = new VaultAddCipherPage(selectedType, defaultFolderId: folderId); - await page.Navigation.PushForDeviceAsync(addPage); - } - - public static async Task AddField(Page page, TableSection fieldsSection) - { - var type = await page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null, - AppResources.FieldTypeText, AppResources.FieldTypeHidden, AppResources.FieldTypeBoolean); - - FieldType fieldType; - if(type == AppResources.FieldTypeText) - { - fieldType = FieldType.Text; - } - else if(type == AppResources.FieldTypeHidden) - { - fieldType = FieldType.Hidden; - } - else if(type == AppResources.FieldTypeBoolean) - { - fieldType = FieldType.Boolean; - } - else - { - return; - } - - var daService = Resolver.Resolve(); - var label = await daService.DisplayPromptAync(AppResources.CustomFieldName); - if(label == null) - { - return; - } - - var cell = MakeFieldCell(fieldType, label, string.Empty, fieldsSection, page); - if(cell != null) - { - fieldsSection.Insert(fieldsSection.Count - 1, cell); - if(cell is FormEntryCell feCell) - { - feCell.InitEvents(); - } - } - } - - public static Cell MakeFieldCell(FieldType type, string label, string value, - TableSection fieldsSection, Page page) - { - Cell cell; - FormEntryCell feCell = null; - FormSwitchCell fsCell = null; - switch(type) - { - case FieldType.Text: - case FieldType.Hidden: - var hidden = type == FieldType.Hidden; - cell = feCell = new FormEntryCell(label, isPassword: hidden, - button1: hidden ? "eye.png" : "cog_alt.png", button2: hidden ? "cog_alt.png" : null); - feCell.Entry.Text = value; - feCell.Entry.DisableAutocapitalize = true; - feCell.Entry.Autocorrect = false; - - if(hidden) - { - feCell.Entry.FontFamily = OnPlatform(iOS: "Menlo-Regular", Android: "monospace", - Windows: "Courier"); - feCell.Button1.Command = new Command(() => - { - feCell.Entry.InvokeToggleIsPassword(); - feCell.Button1.Image = "eye" + - (!feCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - }); - } - break; - case FieldType.Boolean: - cell = fsCell = new FormSwitchCell(label, "cog_alt.png"); - fsCell.Switch.IsToggled = value == "true"; - break; - default: - cell = null; - break; - } - - if(cell != null) - { - var optionsButton = feCell != null ? feCell.Button2 ?? feCell.Button1 : fsCell.Button1; - optionsButton.Command = new Command(async () => - { - var optionsVal = await page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, - null, AppResources.Edit, AppResources.Remove); - if(optionsVal == AppResources.Remove) - { - if(fieldsSection.Contains(cell)) - { - fieldsSection.Remove(cell); - } - - if(feCell != null) - { - feCell.Dispose(); - } - cell = null; - feCell = null; - fsCell = null; - } - else if(optionsVal == AppResources.Edit) - { - var existingLabel = feCell?.Label.Text ?? fsCell?.Label.Text; - var daService = Resolver.Resolve(); - var editLabel = await daService.DisplayPromptAync(AppResources.CustomFieldName, - null, existingLabel); - if(editLabel != null) - { - if(feCell != null) - { - feCell.Label.Text = editLabel; - } - else if(fsCell != null) - { - fsCell.Label.Text = editLabel; - } - } - } - }); - } - - return cell; - } - - public static List> ProcessFieldsSectionForSave(TableSection fieldsSection, Cipher cipher) - { - var hiddenFieldValues = new List>(); - if(fieldsSection != null && fieldsSection.Count > 0) - { - var fields = new List(); - foreach(var cell in fieldsSection) - { - if(cell is FormEntryCell entryCell) - { - var type = entryCell.Entry.IsPassword || entryCell.Button2 != null ? - FieldType.Hidden : FieldType.Text; - fields.Add(new Field - { - Name = string.IsNullOrWhiteSpace(entryCell.Label.Text) ? null : - entryCell.Label.Text.Encrypt(cipher.OrganizationId), - Value = string.IsNullOrWhiteSpace(entryCell.Entry.Text) ? null : - entryCell.Entry.Text.Encrypt(cipher.OrganizationId), - Type = type - }); - - if(type == FieldType.Hidden && !string.IsNullOrWhiteSpace(entryCell.Label.Text)) - { - hiddenFieldValues.Add(new Tuple(entryCell.Label.Text, - entryCell.Entry.Text)); - } - } - else if(cell is FormSwitchCell switchCell) - { - var value = switchCell.Switch.IsToggled ? "true" : "false"; - fields.Add(new Field - { - Name = string.IsNullOrWhiteSpace(switchCell.Label.Text) ? null : - switchCell.Label.Text.Encrypt(cipher.OrganizationId), - Value = value.Encrypt(cipher.OrganizationId), - Type = FieldType.Boolean - }); - } - } - cipher.Fields = fields; - } - - if(!cipher.Fields?.Any() ?? true) - { - cipher.Fields = null; - } - return hiddenFieldValues; - } - - public static FormEntryCell MakeUriCell(string value, UriMatchType? match, TableSection urisSection, Page page) - { - var label = string.Format(AppResources.URIPosition, urisSection.Count); - var cell = new FormEntryCell(label, entryKeyboard: Keyboard.Url, button1: "cog_alt.png"); - cell.Entry.Text = value; - cell.Entry.DisableAutocapitalize = true; - cell.Entry.Autocorrect = false; - cell.MetaData = new Dictionary { ["match"] = match }; - - cell.Button1.Command = new Command(async () => - { - var optionsVal = await page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, - null, AppResources.MatchDetection, AppResources.Remove); - - if(optionsVal == AppResources.MatchDetection) - { - var options = UriMatchOptionsMap.Select(v => v.Value).ToList(); - options.Insert(0, AppResources.Default); - var exactingMatchVal = cell.MetaData["match"] as UriMatchType?; - - var matchIndex = exactingMatchVal.HasValue ? - Array.IndexOf(UriMatchOptionsMap.Keys.ToArray(), exactingMatchVal) + 1 : 0; - options[matchIndex] = $"✓ {options[matchIndex]}"; - - var optionsArr = options.ToArray(); - var val = await page.DisplayActionSheet(AppResources.URIMatchDetection, AppResources.Cancel, - null, options.ToArray()); - - UriMatchType? selectedVal = null; - if(val == null || val == AppResources.Cancel) - { - selectedVal = exactingMatchVal; - } - else if(val.Replace("✓ ", string.Empty) != AppResources.Default) - { - selectedVal = UriMatchOptionsMap.ElementAt(Array.IndexOf(optionsArr, val) - 1).Key; - } - cell.MetaData["match"] = selectedVal; - } - else if(optionsVal == AppResources.Remove) - { - if(urisSection.Contains(cell)) - { - urisSection.Remove(cell); - if(cell is FormEntryCell feCell) - { - feCell.Dispose(); - } - cell = null; - - for(int i = 0; i < urisSection.Count; i++) - { - if(urisSection[i] is FormEntryCell uriCell) - { - uriCell.Label.Text = string.Format(AppResources.URIPosition, i + 1); - } - } - } - } - }); - - return cell; - } - - public static void ProcessUrisSectionForSave(TableSection urisSection, Cipher cipher) - { - if(urisSection != null && urisSection.Count > 0) - { - var uris = new List(); - foreach(var cell in urisSection) - { - if(cell is FormEntryCell entryCell && !string.IsNullOrWhiteSpace(entryCell.Entry.Text)) - { - var match = entryCell?.MetaData["match"] as UriMatchType?; - uris.Add(new LoginUri - { - Uri = entryCell.Entry.Text.Encrypt(cipher.OrganizationId), - Match = match - }); - } - } - cipher.Login.Uris = uris; - } - - if(!cipher.Login.Uris?.Any() ?? true) - { - cipher.Login.Uris = null; - } - } - - public static void InitSectionEvents(TableSection section) - { - if(section != null && section.Count > 0) - { - foreach(var cell in section) - { - if(cell is FormEntryCell entrycell) - { - entrycell.InitEvents(); - } - } - } - } - - public static void DisposeSectionEvents(TableSection section) - { - if(section != null && section.Count > 0) - { - foreach(var cell in section) - { - if(cell is FormEntryCell entrycell) - { - entrycell.Dispose(); - } - } - } - } - - public static string GetUrlHost(string url) - { - if(string.IsNullOrWhiteSpace(url)) - { - return null; - } - - url = url.Trim(); - if(url == string.Empty) - { - return null; - } - - if(!url.Contains("://")) - { - url = $"http://{url}"; - } - - if(!Uri.TryCreate(url, UriKind.Absolute, out Uri u)) - { - return null; - } - - var host = u.Host; - if(!u.IsDefaultPort) - { - host = $"{host}:{u.Port}"; - } - - return host; - } - - public static void AlertNoConnection(Page page) - { - page.DisplayAlert(AppResources.InternetConnectionRequiredTitle, - AppResources.InternetConnectionRequiredMessage, AppResources.Ok); - } - - public static Dictionary GetQueryParams(string urlString) - { - var dict = new Dictionary(); - if(!Uri.TryCreate(urlString, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Query)) - { - return dict; - } - - var pairs = uri.Query.Substring(1).Split('&'); - foreach(var pair in pairs) - { - var parts = pair.Split('='); - if(parts.Length < 1) - { - continue; - } - var key = System.Net.WebUtility.UrlDecode(parts[0]).ToLower(); - if(!dict.ContainsKey(key)) - { - dict.Add(key, parts[1] == null ? string.Empty : System.Net.WebUtility.UrlDecode(parts[1])); - } - } - return dict; - } - - public static bool CanAccessPremium() - { - var tokenService = Resolver.Resolve(); - if(tokenService?.TokenPremium ?? false) - { - return true; - } - var appSettingsService = Resolver.Resolve(); - return appSettingsService?.OrganizationGivesPremium ?? false; - } - - public static void NestedTraverse(List> nodeTree, int partIndex, string[] parts, - T obj, T parent, string delimiter) where T : ITreeNodeObject - { - if(parts.Length <= partIndex) - { - return; - } - - var end = partIndex == parts.Length - 1; - var partName = parts[partIndex]; - - foreach(var n in nodeTree) - { - if(n.Node.Name != parts[partIndex]) - { - continue; - } - if(end && n.Node.Id != obj.Id) - { - // Another node with the same name. - nodeTree.Add(new TreeNode(obj, partName, parent)); - return; - } - NestedTraverse(n.Children, partIndex + 1, parts, obj, n.Node, delimiter); - return; - } - - if(!nodeTree.Any(n => n.Node.Name == partName)) - { - if(end) - { - nodeTree.Add(new TreeNode(obj, partName, parent)); - return; - } - var newPartName = string.Concat(parts[partIndex], delimiter, parts[partIndex + 1]); - var newParts = new List { newPartName }; - var newPartsStartFrom = partIndex + 2; - newParts.AddRange(new ArraySegment(parts, newPartsStartFrom, parts.Length - newPartsStartFrom)); - NestedTraverse(nodeTree, 0, newParts.ToArray(), obj, parent, delimiter); - } - } - - public static TreeNode GetTreeNodeObject(List> nodeTree, string id) where T : ITreeNodeObject - { - foreach(var n in nodeTree) - { - if(n.Node.Id == id) - { - return n; - } - else if(n.Children != null) - { - var node = GetTreeNodeObject(n.Children, id); - if(node != null) - { - return node; - } - } - } - return null; - } - - public static List> GetAllNested(IEnumerable objs) where T : ITreeNodeObject - { - var nodes = new List>(); - foreach(var o in objs) - { - NestedTraverse(nodes, 0, o.Name.Split('/'), o, default(T), "/"); - } - return nodes; - } - } -} diff --git a/src/App/Utilities/I18nExtension.cs b/src/App/Utilities/I18nExtension.cs new file mode 100644 index 000000000..232ddb2af --- /dev/null +++ b/src/App/Utilities/I18nExtension.cs @@ -0,0 +1,29 @@ +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System; +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace Bit.App.Utilities +{ + [ContentProperty("Id")] + public class I18nExtension : IMarkupExtension + { + private II18nService _i18nService; + + public I18nExtension() + { + _i18nService = ServiceContainer.Resolve("i18nService"); + } + + public string Id { get; set; } + public string P1 { get; set; } + public string P2 { get; set; } + public string P3 { get; set; } + + public object ProvideValue(IServiceProvider serviceProvider) + { + return _i18nService.T(Id, P1, P2, P3); + } + } +} diff --git a/src/App/Utilities/IdentityHttpClient.cs b/src/App/Utilities/IdentityHttpClient.cs deleted file mode 100644 index 1ef74d1ea..000000000 --- a/src/App/Utilities/IdentityHttpClient.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Net.Http; -using System; -using System.Net.Http.Headers; -using XLabs.Ioc; -using Bit.App.Abstractions; - -namespace Bit.App -{ - public class IdentityHttpClient : HttpClient - { - public IdentityHttpClient() - { - Init(); - } - - public IdentityHttpClient(HttpMessageHandler handler) - : base(handler) - { - Init(); - } - - private void Init() - { - DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - var appSettings = Resolver.Resolve(); - if(!string.IsNullOrWhiteSpace(appSettings.BaseUrl)) - { - BaseAddress = new Uri($"{appSettings.BaseUrl}/identity"); - } - else if(!string.IsNullOrWhiteSpace(appSettings.IdentityUrl)) - { - BaseAddress = new Uri($"{appSettings.IdentityUrl}"); - } - else - { - //BaseAddress = new Uri("http://169.254.80.80:33656"); // Desktop from VS Android Emulator - //BaseAddress = new Uri("http://192.168.1.3:33656"); // Desktop - //BaseAddress = new Uri("https://preview-identity.bitwarden.com"); // Preview - BaseAddress = new Uri("https://identity.bitwarden.com"); // Production - } - } - } -} diff --git a/src/App/Utilities/InverseBoolConverter.cs b/src/App/Utilities/InverseBoolConverter.cs new file mode 100644 index 000000000..439bbb7f7 --- /dev/null +++ b/src/App/Utilities/InverseBoolConverter.cs @@ -0,0 +1,24 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + if(targetType == typeof(bool)) + { + return !((bool?)value).GetValueOrDefault(); + } + throw new InvalidOperationException("The target must be a boolean."); + } + + public object ConvertBack(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/App/Utilities/IsNotNullConverter.cs b/src/App/Utilities/IsNotNullConverter.cs new file mode 100644 index 000000000..d692e813c --- /dev/null +++ b/src/App/Utilities/IsNotNullConverter.cs @@ -0,0 +1,24 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public class IsNotNullConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + if(targetType == typeof(bool)) + { + return value != null; + } + throw new InvalidOperationException("The target must be a boolean."); + } + + public object ConvertBack(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/App/Utilities/IsNullConverter.cs b/src/App/Utilities/IsNullConverter.cs new file mode 100644 index 000000000..99ac14d1c --- /dev/null +++ b/src/App/Utilities/IsNullConverter.cs @@ -0,0 +1,24 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public class IsNullConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + if(targetType == typeof(bool)) + { + return value == null; + } + throw new InvalidOperationException("The target must be a boolean."); + } + + public object ConvertBack(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/App/Utilities/PasswordFormatter.cs b/src/App/Utilities/PasswordFormatter.cs index 9b90e7db4..0e20eb2e9 100644 --- a/src/App/Utilities/PasswordFormatter.cs +++ b/src/App/Utilities/PasswordFormatter.cs @@ -24,7 +24,6 @@ namespace Bit.App.Utilities public static FormattedString FormatPassword(String password) { var result = new FormattedString(); - // Start off with an empty span to prevent possible NPEs. Due to the way the state machine // works, this will actually always be replaced by a new span anyway. var currentSpan = new Span(); @@ -61,17 +60,15 @@ namespace Bit.App.Utilities switch(currentType) { case CharType.Number: - currentSpan.TextColor = Color.DodgerBlue; + currentSpan.TextColor = (Color)Application.Current.Resources["PasswordNumberColor"]; break; case CharType.Special: - currentSpan.TextColor = Color.Firebrick; + currentSpan.TextColor = (Color)Application.Current.Resources["PasswordSpecialColor"]; break; } } - currentSpan.Text += c; } - return result; } } diff --git a/src/App/Utilities/StringHasValueConverter.cs b/src/App/Utilities/StringHasValueConverter.cs new file mode 100644 index 000000000..0af574ebc --- /dev/null +++ b/src/App/Utilities/StringHasValueConverter.cs @@ -0,0 +1,31 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public class StringHasValueConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + if(targetType == typeof(bool)) + { + if(value == null) + { + return false; + } + if(value.GetType() == typeof(string)) + { + return !string.IsNullOrWhiteSpace((string)value); + } + } + throw new InvalidOperationException("The value must be a string with a boolean target."); + } + + public object ConvertBack(object value, Type targetType, object parameter, + System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs new file mode 100644 index 000000000..0a1b0ca85 --- /dev/null +++ b/src/App/Utilities/ThemeManager.cs @@ -0,0 +1,44 @@ +using Bit.App.Styles; +using System; +using System.Reflection; +using Xamarin.Forms; +using Xamarin.Forms.StyleSheets; + +namespace Bit.App.Utilities +{ + public static class ThemeManager + { + public static void SetThemeStyle(string name) + { + // Reset styles + Application.Current.Resources.Clear(); + Application.Current.Resources.MergedDictionaries.Clear(); + + // Variables + Application.Current.Resources.MergedDictionaries.Add(new Variables()); + + // Themed variables + if(name == "dark") + { + Application.Current.Resources.MergedDictionaries.Add(new Dark()); + } + else + { + Application.Current.Resources.MergedDictionaries.Add(new Light()); + } + + // Base styles + Application.Current.Resources.MergedDictionaries.Add(new Base()); + + // Platform styles + if(Device.RuntimePlatform == Device.Android) + { + Application.Current.Resources.MergedDictionaries.Add(new Android()); + } + else if(Device.RuntimePlatform == Device.iOS) + { + Application.Current.Resources.MergedDictionaries.Add(new iOS()); + } + } + } +} diff --git a/src/App/Utilities/TokenHttpRequestMessage.cs b/src/App/Utilities/TokenHttpRequestMessage.cs deleted file mode 100644 index c8e6812a2..000000000 --- a/src/App/Utilities/TokenHttpRequestMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net.Http; -using System.Text; -using Bit.App.Abstractions; -using Bit.App.Enums; -using Bit.App.Utilities; -using Newtonsoft.Json; -using XLabs.Ioc; - -namespace Bit.App -{ - public class TokenHttpRequestMessage : HttpRequestMessage - { - public TokenHttpRequestMessage() - { - var tokenService = Resolver.Resolve(); - var appIdService = Resolver.Resolve(); - var deviceInfoService = Resolver.Resolve(); - - if(!string.IsNullOrWhiteSpace(tokenService.Token)) - { - Headers.Add("Authorization", $"Bearer {tokenService.Token}"); - } - if(!string.IsNullOrWhiteSpace(appIdService.AppId)) - { - Headers.Add("Device-Identifier", appIdService.AppId); - } - - Headers.Add("Device-Type", ((int)Helpers.OnPlatform(iOS: DeviceType.iOS, - Android: DeviceType.Android, Windows: DeviceType.UWP, platform: deviceInfoService.Type)).ToString()); - } - - public TokenHttpRequestMessage(object requestObject) - : this() - { - var stringContent = JsonConvert.SerializeObject(requestObject); - Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); - } - } -} diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs new file mode 100644 index 000000000..a44665afb --- /dev/null +++ b/src/Core/Abstractions/IApiService.cs @@ -0,0 +1,50 @@ +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IApiService + { + string ApiBaseUrl { get; set; } + string IdentityBaseUrl { get; set; } + bool UrlsSet { get; } + + Task DeleteCipherAsync(string id); + Task DeleteCipherAttachmentAsync(string id, string attachmentId); + Task DeleteFolderAsync(string id); + Task DoRefreshTokenAsync(); + Task GetAccountRevisionDateAsync(); + Task GetActiveBearerTokenAsync(); + Task GetCipherAsync(string id); + Task GetFolderAsync(string id); + Task GetProfileAsync(); + Task GetSyncAsync(); + Task PostAccountKeysAsync(KeysRequest request); + Task PostCipherAsync(CipherRequest request); + Task PostCipherCreateAsync(CipherCreateRequest request); + Task PostFolderAsync(FolderRequest request); + Task> PostIdentityTokenAsync(TokenRequest request); + Task PostPasswordHintAsync(PasswordHintRequest request); + Task PostPreloginAsync(PreloginRequest request); + Task PostRegisterAsync(RegisterRequest request); + Task PutCipherAsync(string id, CipherRequest request); + Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request); + Task PutFolderAsync(string id, FolderRequest request); + Task PutShareCipherAsync(string id, CipherShareRequest request); + Task RefreshIdentityTokenAsync(); + Task SendAsync(HttpMethod method, string path, + TRequest body, bool authed, bool hasResponse); + void SetUrls(EnvironmentUrls urls); + Task PostCipherAttachmentAsync(string id, MultipartFormDataContent data); + Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, + string organizationId); + Task> GetHibpBreachAsync(string username); + Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request); + Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request); + } +} diff --git a/src/Core/Abstractions/IAppIdService.cs b/src/Core/Abstractions/IAppIdService.cs new file mode 100644 index 000000000..6c3bd6d30 --- /dev/null +++ b/src/Core/Abstractions/IAppIdService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IAppIdService + { + Task GetAppIdAsync(); + Task GetAnonymousAppIdAsync(); + } +} diff --git a/src/Core/Abstractions/IAuditService.cs b/src/Core/Abstractions/IAuditService.cs new file mode 100644 index 000000000..f28bb2e6f --- /dev/null +++ b/src/Core/Abstractions/IAuditService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface IAuditService + { + Task> BreachedAccountsAsync(string username); + Task PasswordLeakedAsync(string password); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs new file mode 100644 index 000000000..538abf338 --- /dev/null +++ b/src/Core/Abstractions/IAuthService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IAuthService + { + string Email { get; set; } + string MasterPasswordHash { get; set; } + TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } + Dictionary TwoFactorProviders { get; set; } + Dictionary> TwoFactorProvidersData { get; set; } + + TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported); + List GetSupportedTwoFactorProviders(); + Task LogInAsync(string email, string masterPassword); + Task LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null); + Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null); + void LogOut(Action callback); + void Init(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IBroadcasterService.cs b/src/Core/Abstractions/IBroadcasterService.cs new file mode 100644 index 000000000..afdf29f21 --- /dev/null +++ b/src/Core/Abstractions/IBroadcasterService.cs @@ -0,0 +1,12 @@ +using Bit.Core.Models.Domain; +using System; + +namespace Bit.Core.Abstractions +{ + public interface IBroadcasterService + { + void Send(Message message, string id = null); + void Subscribe(string id, Action messageCallback); + void Unsubscribe(string id); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs new file mode 100644 index 000000000..ff6f31c09 --- /dev/null +++ b/src/Core/Abstractions/ICipherService.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ICipherService + { + Task ClearAsync(string userId); + void ClearCache(); + Task DeleteAsync(List ids); + Task DeleteAsync(string id); + Task DeleteAttachmentAsync(string id, string attachmentId); + Task DeleteAttachmentWithServerAsync(string id, string attachmentId); + Task DeleteWithServerAsync(string id); + Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task, List, List>> GetAllDecryptedByUrlAsync(string url, + List includeOtherTypes = null); + Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true); + Task> GetAllDecryptedForUrlAsync(string url); + Task GetAsync(string id); + Task GetLastUsedForUrlAsync(string url); + Task ReplaceAsync(Dictionary ciphers); + Task SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data); + Task SaveCollectionsWithServerAsync(Cipher cipher); + Task SaveNeverDomainAsync(string domain); + Task SaveWithServerAsync(Cipher cipher); + Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet collectionIds); + Task UpdateLastUsedDateAsync(string id); + Task UpsertAsync(CipherData cipher); + Task UpsertAsync(List cipher); + Task DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ICollectionService.cs b/src/Core/Abstractions/ICollectionService.cs new file mode 100644 index 000000000..1f649dba4 --- /dev/null +++ b/src/Core/Abstractions/ICollectionService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ICollectionService + { + Task ClearAsync(string userId); + void ClearCache(); + Task> DecryptManyAsync(List collections); + Task DeleteAsync(string id); + Task EncryptAsync(CollectionView model); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task>> GetAllNestedAsync(List collections = null); + Task GetAsync(string id); + Task> GetNestedAsync(string id); + Task ReplaceAsync(Dictionary collections); + Task UpsertAsync(CollectionData collection); + Task UpsertAsync(List collection); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs new file mode 100644 index 000000000..cb1e5a2a7 --- /dev/null +++ b/src/Core/Abstractions/ICryptoFunctionService.cs @@ -0,0 +1,28 @@ +using Bit.Core.Enums; +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoFunctionService + { + Task Pbkdf2Async(string password, string salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + Task Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + Task HashAsync(string value, CryptoHashAlgorithm algorithm); + Task HashAsync(byte[] value, CryptoHashAlgorithm algorithm); + Task HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm); + Task CompareAsync(byte[] a, byte[] b); + Task AesEncryptAsync(byte[] data, byte[] iv, byte[] key); + Task AesDecryptAsync(byte[] data, byte[] iv, byte[] key); + Task RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm); + Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm); + Task RsaExtractPublicKeyAsync(byte[] privateKey); + Task> RsaGenerateKeyPairAsync(int length); + Task RandomBytesAsync(int length); + byte[] RandomBytes(int length); + Task RandomNumberAsync(); + uint RandomNumber(); + } +} diff --git a/src/Core/Abstractions/ICryptoPrimitiveService.cs b/src/Core/Abstractions/ICryptoPrimitiveService.cs new file mode 100644 index 000000000..897c337c4 --- /dev/null +++ b/src/Core/Abstractions/ICryptoPrimitiveService.cs @@ -0,0 +1,9 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoPrimitiveService + { + byte[] Pbkdf2(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations); + } +} diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs new file mode 100644 index 000000000..6df2cc9f2 --- /dev/null +++ b/src/Core/Abstractions/ICryptoService.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface ICryptoService + { + Task ClearEncKeyAsync(bool memoryOnly = false); + Task ClearKeyAsync(); + Task ClearKeyHashAsync(); + Task ClearKeyPairAsync(bool memoryOnly = false); + Task ClearKeysAsync(); + Task ClearOrgKeysAsync(bool memoryOnly = false); + Task ClearPinProtectedKeyAsync(); + Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key); + Task DecryptToBytesAsync(CipherString cipherString, SymmetricCryptoKey key = null); + Task DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null); + Task EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null); + Task EncryptAsync(string plainValue, SymmetricCryptoKey key = null); + Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null); + Task GetEncKeyAsync(); + Task> GetFingerprintAsync(string userId, byte[] publicKey = null); + Task GetKeyAsync(); + Task GetKeyHashAsync(); + Task GetOrgKeyAsync(string orgId); + Task> GetOrgKeysAsync(); + Task GetPrivateKeyAsync(); + Task GetPublicKeyAsync(); + Task HasEncKeyAsync(); + Task HashPasswordAsync(string password, SymmetricCryptoKey key); + Task HasKeyAsync(); + Task> MakeEncKeyAsync(SymmetricCryptoKey key); + Task MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations); + Task MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations); + Task> MakeKeyPairAsync(SymmetricCryptoKey key = null); + Task MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations); + Task> MakeShareKeyAsync(); + Task RandomNumberAsync(int min, int max); + Task> RemakeEncKeyAsync(SymmetricCryptoKey key); + Task RsaEncryptAsync(byte[] data, byte[] publicKey = null); + Task SetEncKeyAsync(string encKey); + Task SetEncPrivateKeyAsync(string encPrivateKey); + Task SetKeyAsync(SymmetricCryptoKey key); + Task SetKeyHashAsync(string keyHash); + Task SetOrgKeysAsync(IEnumerable orgs); + Task ToggleKeyAsync(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IEnvironmentService.cs b/src/Core/Abstractions/IEnvironmentService.cs new file mode 100644 index 000000000..5df4e0dd2 --- /dev/null +++ b/src/Core/Abstractions/IEnvironmentService.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Data; + +namespace Bit.Core.Abstractions +{ + public interface IEnvironmentService + { + string ApiUrl { get; set; } + string BaseUrl { get; set; } + string IconsUrl { get; set; } + string IdentityUrl { get; set; } + string NotificationsUrl { get; set; } + string WebVaultUrl { get; set; } + + string GetWebVaultUrl(); + Task SetUrlsAsync(EnvironmentUrlData urls); + Task SetUrlsFromStorageAsync(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IFolderService.cs b/src/Core/Abstractions/IFolderService.cs new file mode 100644 index 000000000..818921fa6 --- /dev/null +++ b/src/Core/Abstractions/IFolderService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface IFolderService + { + Task ClearAsync(string userId); + void ClearCache(); + Task DeleteAsync(string id); + Task DeleteWithServerAsync(string id); + Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null); + Task> GetAllAsync(); + Task> GetAllDecryptedAsync(); + Task>> GetAllNestedAsync(); + Task GetAsync(string id); + Task> GetNestedAsync(string id); + Task ReplaceAsync(Dictionary folders); + Task SaveWithServerAsync(Folder folder); + Task UpsertAsync(FolderData folder); + Task UpsertAsync(List folder); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/II18nService.cs b/src/Core/Abstractions/II18nService.cs new file mode 100644 index 000000000..69e1b8fe2 --- /dev/null +++ b/src/Core/Abstractions/II18nService.cs @@ -0,0 +1,13 @@ +using System; +using System.Globalization; + +namespace Bit.Core.Abstractions +{ + public interface II18nService + { + CultureInfo Culture { get; set; } + StringComparer StringComparer { get; } + string T(string id, string p1 = null, string p2 = null, string p3 = null); + string Translate(string id, string p1 = null, string p2 = null, string p3 = null); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ILockService.cs b/src/Core/Abstractions/ILockService.cs new file mode 100644 index 000000000..d6c167f85 --- /dev/null +++ b/src/Core/Abstractions/ILockService.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ILockService + { + bool PinLocked { get; set; } + bool FingerprintLocked { get; set; } + + Task CheckLockAsync(); + Task ClearAsync(); + Task IsLockedAsync(); + Task> IsPinLockSetAsync(); + Task IsFingerprintLockSetAsync(); + Task LockAsync(bool allowSoftLock = false); + Task SetLockOptionAsync(int? lockOption); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IMessagingService.cs b/src/Core/Abstractions/IMessagingService.cs new file mode 100644 index 000000000..08d631907 --- /dev/null +++ b/src/Core/Abstractions/IMessagingService.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Abstractions +{ + public interface IMessagingService + { + void Send(string subscriber, object arg = null); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IPasswordGenerationService.cs b/src/Core/Abstractions/IPasswordGenerationService.cs new file mode 100644 index 000000000..d4e37d603 --- /dev/null +++ b/src/Core/Abstractions/IPasswordGenerationService.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IPasswordGenerationService + { + Task AddHistoryAsync(string password); + Task ClearAsync(); + Task GeneratePassphraseAsync(PasswordGenerationOptions options); + Task GeneratePasswordAsync(PasswordGenerationOptions options); + Task> GetHistoryAsync(); + Task GetOptionsAsync(); + Task PasswordStrength(string password, List userInputs = null); + Task SaveOptionsAsync(PasswordGenerationOptions options); + void NormalizeOptions(PasswordGenerationOptions options); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs new file mode 100644 index 000000000..b0ac5354a --- /dev/null +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IPlatformUtilsService + { + string IdentityClientId { get; } + + Task CopyToClipboardAsync(string text, Dictionary options = null); + string GetApplicationVersion(); + DeviceType GetDevice(); + string GetDeviceString(); + bool IsDev(); + bool IsSelfHost(); + bool IsViewOpen(); + void LaunchUri(string uri, Dictionary options = null); + int? LockTimeout(); + Task ReadFromClipboardAsync(Dictionary options = null); + void SaveFile(); + Task ShowDialogAsync(string text, string title = null, string confirmText = null, + string cancelText = null, string type = null); + void ShowToast(string type, string title, string text, Dictionary options = null); + void ShowToast(string type, string title, string[] text, Dictionary options = null); + bool SupportsU2f(); + bool SupportsDuo(); + Task SupportsFingerprintAsync(); + Task AuthenticateFingerprintAsync(string text = null, string fallbackText = null, Action fallback = null); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ISearchService.cs b/src/Core/Abstractions/ISearchService.cs new file mode 100644 index 000000000..fae344bd8 --- /dev/null +++ b/src/Core/Abstractions/ISearchService.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core.Models.View; + +namespace Bit.Core.Abstractions +{ + public interface ISearchService + { + void ClearIndex(); + Task IndexCiphersAsync(); + bool IsSearchable(string query); + Task> SearchCiphersAsync(string query, Func filter = null, + List ciphers = null, CancellationToken ct = default(CancellationToken)); + List SearchCiphersBasic(List ciphers, string query, + CancellationToken ct = default(CancellationToken)); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ISettingsService.cs b/src/Core/Abstractions/ISettingsService.cs new file mode 100644 index 000000000..a5b780e8d --- /dev/null +++ b/src/Core/Abstractions/ISettingsService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ISettingsService + { + Task ClearAsync(string userId); + void ClearCache(); + Task>> GetEquivalentDomainsAsync(); + Task SetEquivalentDomainsAsync(List> equivalentDomains); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs new file mode 100644 index 000000000..484ed6d52 --- /dev/null +++ b/src/Core/Abstractions/IStateService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStateService + { + Task GetAsync(string key); + Task RemoveAsync(string key); + Task SaveAsync(string key, T obj); + Task PurgeAsync(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IStorageService.cs b/src/Core/Abstractions/IStorageService.cs new file mode 100644 index 000000000..5ff667cc7 --- /dev/null +++ b/src/Core/Abstractions/IStorageService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStorageService + { + Task GetAsync(string key); + Task SaveAsync(string key, T obj); + Task RemoveAsync(string key); + } +} diff --git a/src/Core/Abstractions/ISyncService.cs b/src/Core/Abstractions/ISyncService.cs new file mode 100644 index 000000000..285d974b4 --- /dev/null +++ b/src/Core/Abstractions/ISyncService.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface ISyncService + { + bool SyncInProgress { get; set; } + + Task FullSyncAsync(bool forceSync); + Task GetLastSyncAsync(); + Task SetLastSyncAsync(DateTime date); + Task SyncDeleteCipherAsync(SyncCipherNotification notification); + Task SyncDeleteFolderAsync(SyncFolderNotification notification); + Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit); + Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ITokenService.cs b/src/Core/Abstractions/ITokenService.cs new file mode 100644 index 000000000..96a3fc62f --- /dev/null +++ b/src/Core/Abstractions/ITokenService.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Abstractions +{ + public interface ITokenService + { + Task ClearTokenAsync(); + Task ClearTwoFactorTokenAsync(string email); + JObject DecodeToken(); + string GetEmail(); + bool GetEmailVerified(); + string GetIssuer(); + string GetName(); + bool GetPremium(); + Task GetRefreshTokenAsync(); + Task GetTokenAsync(); + DateTime? GetTokenExpirationDate(); + Task GetTwoFactorTokenAsync(string email); + string GetUserId(); + Task SetRefreshTokenAsync(string refreshToken); + Task SetTokenAsync(string token); + Task SetTokensAsync(string accessToken, string refreshToken); + Task SetTwoFactorTokenAsync(string token, string email); + bool TokenNeedsRefresh(int minutes = 5); + int TokenSecondsRemaining(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/ITotpService.cs b/src/Core/Abstractions/ITotpService.cs new file mode 100644 index 000000000..0de407e3e --- /dev/null +++ b/src/Core/Abstractions/ITotpService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface ITotpService + { + Task GetCodeAsync(string key); + int GetTimeInterval(string key); + Task IsAutoCopyEnabledAsync(); + } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IUserService.cs b/src/Core/Abstractions/IUserService.cs new file mode 100644 index 000000000..b282acf54 --- /dev/null +++ b/src/Core/Abstractions/IUserService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IUserService + { + Task CanAccessPremiumAsync(); + Task ClearAsync(); + Task ClearOrganizationsAsync(string userId); + Task> GetAllOrganizationAsync(); + Task GetEmailAsync(); + Task GetKdfAsync(); + Task GetKdfIterationsAsync(); + Task GetOrganizationAsync(string id); + Task GetSecurityStampAsync(); + Task GetUserIdAsync(); + Task IsAuthenticatedAsync(); + Task ReplaceOrganizationsAsync(Dictionary organizations); + Task SetInformationAsync(string userId, string email, KdfType kdf, int kdfIterations); + Task SetSecurityStampAsync(string stamp); + } +} \ No newline at end of file diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs new file mode 100644 index 000000000..8211aad70 --- /dev/null +++ b/src/Core/Constants.cs @@ -0,0 +1,26 @@ +namespace Bit.Core +{ + public static class Constants + { + public const string AndroidAppProtocol = "androidapp://"; + public const string iOSAppProtocol = "iosapp://"; + public static string LockOptionKey = "lockOption"; + public static string LastActiveKey = "lastActive"; + public static string FingerprintUnlockKey = "fingerprintUnlock"; + public static string ProtectedPin = "protectedPin"; + public static string PinProtectedKey = "pinProtectedKey"; + public static string DefaultUriMatch = "defaultUriMatch"; + public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; + public static string EnvironmentUrlsKey = "environmentUrls"; + public static string LastFileCacheClearKey = "lastFileCacheClear"; + public static string AccessibilityAutofillPasswordFieldKey = "accessibilityAutofillPasswordField"; + public static string AccessibilityAutofillPersistNotificationKey = "accessibilityAutofillPersistNotification"; + public static string DisableFaviconKey = "disableFavicon"; + public static string PushRegisteredTokenKey = "pushRegisteredToken"; + public static string PushCurrentTokenKey = "pushCurrentToken"; + public static string PushLastRegistrationDateKey = "pushLastRegistrationDate"; + public static string PushInitialPromptShownKey = "pushInitialPromptShown"; + public const int SelectFileRequestCode = 42; + public const int SelectFilePermissionRequestCode = 43; + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj new file mode 100644 index 000000000..575244ce3 --- /dev/null +++ b/src/Core/Core.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + Bit.Core + BitwardenCore + + + + pdbonly + true + + + + + + + + + + + + + + + + + diff --git a/src/App/Enums/CipherType.cs b/src/Core/Enums/CipherType.cs similarity index 56% rename from src/App/Enums/CipherType.cs rename to src/Core/Enums/CipherType.cs index 94f8785ec..0aca94864 100644 --- a/src/App/Enums/CipherType.cs +++ b/src/Core/Enums/CipherType.cs @@ -1,8 +1,8 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { - public enum CipherType : short + public enum CipherType : byte { - // Folder deprecated + // Folder is deprecated //Folder = 0, Login = 1, SecureNote = 2, diff --git a/src/Core/Enums/CryptoHashAlgorithm.cs b/src/Core/Enums/CryptoHashAlgorithm.cs new file mode 100644 index 000000000..eacdfdfee --- /dev/null +++ b/src/Core/Enums/CryptoHashAlgorithm.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum CryptoHashAlgorithm : byte + { + Sha1 = 0, + Sha256 = 1, + Sha512 = 2, + Md5 = 3 + } +} diff --git a/src/Core/Enums/DeviceType.cs b/src/Core/Enums/DeviceType.cs new file mode 100644 index 000000000..69b740756 --- /dev/null +++ b/src/Core/Enums/DeviceType.cs @@ -0,0 +1,27 @@ +namespace Bit.Core.Enums +{ + public enum DeviceType : byte + { + Android = 0, + iOS = 1, + ChromeExtension = 2, + FirefoxExtension = 3, + OperaExtension = 4, + EdgeExtension = 5, + WindowsDesktop = 6, + MacOsDesktop = 7, + LinuxDesktop = 8, + ChromeBrowser = 9, + FirefoxBrowser = 10, + OperaBrowser = 11, + EdgeBrowser = 12, + IEBrowser = 13, + UnknownBrowser = 14, + AndroidAmazon = 15, + UWP = 16, + SafariBrowser = 17, + VivaldiBrowser = 18, + VivaldiExtension = 19, + SafariExtension = 20 + } +} diff --git a/src/App/Enums/EncryptionType.cs b/src/Core/Enums/EncryptionType.cs similarity index 91% rename from src/App/Enums/EncryptionType.cs rename to src/Core/Enums/EncryptionType.cs index cfa153462..2b6eaf086 100644 --- a/src/App/Enums/EncryptionType.cs +++ b/src/Core/Enums/EncryptionType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum EncryptionType : byte { diff --git a/src/App/Enums/FieldType.cs b/src/Core/Enums/FieldType.cs similarity index 79% rename from src/App/Enums/FieldType.cs rename to src/Core/Enums/FieldType.cs index 50d9251ee..bf30d4f12 100644 --- a/src/App/Enums/FieldType.cs +++ b/src/Core/Enums/FieldType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum FieldType : byte { diff --git a/src/Core/Enums/KdfType.cs b/src/Core/Enums/KdfType.cs new file mode 100644 index 000000000..1c845846a --- /dev/null +++ b/src/Core/Enums/KdfType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Enums +{ + public enum KdfType : byte + { + PBKDF2_SHA256 = 0 + } +} diff --git a/src/App/Enums/PushType.cs b/src/Core/Enums/NotificationType.cs similarity index 83% rename from src/App/Enums/PushType.cs rename to src/Core/Enums/NotificationType.cs index 786c25078..2e35dbf9b 100644 --- a/src/App/Enums/PushType.cs +++ b/src/Core/Enums/NotificationType.cs @@ -1,6 +1,6 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { - public enum PushType : short + public enum NotificationType : byte { SyncCipherUpdate = 0, SyncCipherCreate = 1, diff --git a/src/App/Enums/OrganizationUserStatusType.cs b/src/Core/Enums/OrganizationUserStatusType.cs similarity index 82% rename from src/App/Enums/OrganizationUserStatusType.cs rename to src/Core/Enums/OrganizationUserStatusType.cs index de0c87d57..7d2246dc8 100644 --- a/src/App/Enums/OrganizationUserStatusType.cs +++ b/src/Core/Enums/OrganizationUserStatusType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum OrganizationUserStatusType : byte { diff --git a/src/App/Enums/OrganizationUserType.cs b/src/Core/Enums/OrganizationUserType.cs similarity index 59% rename from src/App/Enums/OrganizationUserType.cs rename to src/Core/Enums/OrganizationUserType.cs index 7fd409193..7021952ef 100644 --- a/src/App/Enums/OrganizationUserType.cs +++ b/src/Core/Enums/OrganizationUserType.cs @@ -1,9 +1,10 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum OrganizationUserType : byte { Owner = 0, Admin = 1, - User = 2 + User = 2, + Manager = 3, } } diff --git a/src/Core/Enums/PaymentMethodType.cs b/src/Core/Enums/PaymentMethodType.cs new file mode 100644 index 000000000..0821f211b --- /dev/null +++ b/src/Core/Enums/PaymentMethodType.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Enums +{ + public enum PaymentMethodType : byte + { + Card = 0, + BankAccount = 1, + PayPal = 2, + BitPay = 3, + Credit = 4, + WireTransfer = 5, + } +} diff --git a/src/Core/Enums/PlanType.cs b/src/Core/Enums/PlanType.cs new file mode 100644 index 000000000..4381ec511 --- /dev/null +++ b/src/Core/Enums/PlanType.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum PlanType : byte + { + Free = 0, + FamiliesAnnually = 1, + TeamsMonthly = 2, + TeamsAnnually = 3, + EnterpriseMonthly = 4, + EnterpriseAnnually = 5, + Custom = 6 + } +} diff --git a/src/App/Enums/SecureNoteType.cs b/src/Core/Enums/SecureNoteType.cs similarity index 72% rename from src/App/Enums/SecureNoteType.cs rename to src/Core/Enums/SecureNoteType.cs index 0bbea38f2..cc84edfc3 100644 --- a/src/App/Enums/SecureNoteType.cs +++ b/src/Core/Enums/SecureNoteType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum SecureNoteType : byte { diff --git a/src/Core/Enums/TransactionType.cs b/src/Core/Enums/TransactionType.cs new file mode 100644 index 000000000..45baa68c0 --- /dev/null +++ b/src/Core/Enums/TransactionType.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Enums +{ + public enum TransactionType : byte + { + Charge = 0, + Credit = 1, + PromotionalCredit = 2, + ReferralCredit = 3, + Refund = 4, + } +} diff --git a/src/App/Enums/TwoFactorProviderType.cs b/src/Core/Enums/TwoFactorProviderType.cs similarity index 88% rename from src/App/Enums/TwoFactorProviderType.cs rename to src/Core/Enums/TwoFactorProviderType.cs index 38798bdbf..1a1b7ffc9 100644 --- a/src/App/Enums/TwoFactorProviderType.cs +++ b/src/Core/Enums/TwoFactorProviderType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum TwoFactorProviderType : byte { diff --git a/src/App/Enums/UriMatchType.cs b/src/Core/Enums/UriMatchType.cs similarity index 86% rename from src/App/Enums/UriMatchType.cs rename to src/Core/Enums/UriMatchType.cs index 760db3be9..569437298 100644 --- a/src/App/Enums/UriMatchType.cs +++ b/src/Core/Enums/UriMatchType.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Enums +namespace Bit.Core.Enums { public enum UriMatchType : byte { diff --git a/src/Core/Exceptions/ApiException.cs b/src/Core/Exceptions/ApiException.cs new file mode 100644 index 000000000..bf0aa9791 --- /dev/null +++ b/src/Core/Exceptions/ApiException.cs @@ -0,0 +1,20 @@ +using Bit.Core.Models.Response; +using System; + +namespace Bit.Core.Exceptions +{ + public class ApiException : Exception + { + public ApiException() + : base("An API error has occurred.") + { } + + public ApiException(ErrorResponse error) + : this() + { + Error = error; + } + + public ErrorResponse Error { get; set; } + } +} diff --git a/src/Core/Models/Api/CardApi.cs b/src/Core/Models/Api/CardApi.cs new file mode 100644 index 000000000..4d5830363 --- /dev/null +++ b/src/Core/Models/Api/CardApi.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Api +{ + public class CardApi + { + public string CardholderName { get; set; } + public string Brand { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + } +} diff --git a/src/Core/Models/Api/FieldApi.cs b/src/Core/Models/Api/FieldApi.cs new file mode 100644 index 000000000..8fca5ac29 --- /dev/null +++ b/src/Core/Models/Api/FieldApi.cs @@ -0,0 +1,11 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class FieldApi + { + public FieldType Type { get; set; } + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Core/Models/Api/IdentityApi.cs b/src/Core/Models/Api/IdentityApi.cs new file mode 100644 index 000000000..b7a9cfd11 --- /dev/null +++ b/src/Core/Models/Api/IdentityApi.cs @@ -0,0 +1,24 @@ +namespace Bit.Core.Models.Api +{ + public class IdentityApi + { + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + } +} diff --git a/src/Core/Models/Api/LoginApi.cs b/src/Core/Models/Api/LoginApi.cs new file mode 100644 index 000000000..b611c2eb8 --- /dev/null +++ b/src/Core/Models/Api/LoginApi.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class LoginApi + { + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + } +} diff --git a/src/Core/Models/Api/LoginUriApi.cs b/src/Core/Models/Api/LoginUriApi.cs new file mode 100644 index 000000000..d9e59c65d --- /dev/null +++ b/src/Core/Models/Api/LoginUriApi.cs @@ -0,0 +1,10 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class LoginUriApi + { + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Core/Models/Api/SecureNoteApi.cs b/src/Core/Models/Api/SecureNoteApi.cs new file mode 100644 index 000000000..f311cfaa0 --- /dev/null +++ b/src/Core/Models/Api/SecureNoteApi.cs @@ -0,0 +1,9 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class SecureNoteApi + { + public SecureNoteType Type { get; set; } + } +} diff --git a/src/Core/Models/Data/AttachmentData.cs b/src/Core/Models/Data/AttachmentData.cs new file mode 100644 index 000000000..32fb585e4 --- /dev/null +++ b/src/Core/Models/Data/AttachmentData.cs @@ -0,0 +1,26 @@ +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class AttachmentData : Data + { + public AttachmentData() { } + + public AttachmentData(AttachmentResponse response) + { + Id = response.Id; + Url = response.Url; + FileName = response.FileName; + Key = response.Key; + Size = response.Size; + SizeName = response.SizeName; + } + + public string Id { get; set; } + public string Url { get; set; } + public string FileName { get; set; } + public string Key { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + } +} diff --git a/src/Core/Models/Data/CardData.cs b/src/Core/Models/Data/CardData.cs new file mode 100644 index 000000000..43744f5c2 --- /dev/null +++ b/src/Core/Models/Data/CardData.cs @@ -0,0 +1,26 @@ +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class CardData : Data + { + public CardData() { } + + public CardData(CardApi data) + { + CardholderName = data.CardholderName; + Brand = data.Brand; + Number = data.Number; + ExpMonth = data.ExpMonth; + ExpYear = data.ExpYear; + Code = data.Code; + } + + public string CardholderName { get; set; } + public string Brand { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + } +} diff --git a/src/Core/Models/Data/CipherData.cs b/src/Core/Models/Data/CipherData.cs new file mode 100644 index 000000000..f56d82465 --- /dev/null +++ b/src/Core/Models/Data/CipherData.cs @@ -0,0 +1,70 @@ +using Bit.Core.Models.Response; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Data +{ + public class CipherData : Data + { + public CipherData() { } + + public CipherData(CipherResponse response, string userId = null, HashSet collectionIds = null) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + FolderId = response.FolderId; + UserId = userId; + Edit = response.Edit; + OrganizationUseTotp = response.OrganizationUseTotp; + Favorite = response.Favorite; + RevisionDate = response.RevisionDate; + Type = response.Type; + Name = response.Name; + Notes = response.Notes; + CollectionIds = collectionIds?.ToList() ?? response.CollectionIds; + + switch(Type) + { + case Enums.CipherType.Login: + Login = new LoginData(response.Login); + break; + case Enums.CipherType.SecureNote: + SecureNote = new SecureNoteData(response.SecureNote); + break; + case Enums.CipherType.Card: + Card = new CardData(response.Card); + break; + case Enums.CipherType.Identity: + Identity = new IdentityData(response.Identity); + break; + default: + break; + } + + Fields = response.Fields?.Select(f => new FieldData(f)).ToList(); + Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList(); + PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList(); + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string UserId { get; set; } + public bool Edit { get; set; } + public bool OrganizationUseTotp { get; set; } + public bool Favorite { get; set; } + public DateTime RevisionDate { get; set; } + public Enums.CipherType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public LoginData Login { get; set; } + public SecureNoteData SecureNote { get; set; } + public CardData Card { get; set; } + public IdentityData Identity { get; set; } + public List Fields { get; set; } + public List Attachments { get; set; } + public List PasswordHistory { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Core/Models/Data/CollectionData.cs b/src/Core/Models/Data/CollectionData.cs new file mode 100644 index 000000000..67d70bfd9 --- /dev/null +++ b/src/Core/Models/Data/CollectionData.cs @@ -0,0 +1,24 @@ +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class CollectionData : Data + { + public CollectionData() { } + + public CollectionData(CollectionDetailsResponse response) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + Name = response.Name; + ExternalId = response.ExternalId; + ReadOnly = response.ReadOnly; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + } +} diff --git a/src/Core/Models/Data/Data.cs b/src/Core/Models/Data/Data.cs new file mode 100644 index 000000000..2fae03c94 --- /dev/null +++ b/src/Core/Models/Data/Data.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Data +{ + public abstract class Data + { + } +} diff --git a/src/Core/Models/Data/EnvironmentUrlData.cs b/src/Core/Models/Data/EnvironmentUrlData.cs new file mode 100644 index 000000000..ae510a264 --- /dev/null +++ b/src/Core/Models/Data/EnvironmentUrlData.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Data +{ + public class EnvironmentUrlData + { + public string Base { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + public string Icons { get; set; } + public string Notifications { get; set; } + public string WebVault { get; set; } + } +} diff --git a/src/Core/Models/Data/FieldData.cs b/src/Core/Models/Data/FieldData.cs new file mode 100644 index 000000000..9e7bf67e9 --- /dev/null +++ b/src/Core/Models/Data/FieldData.cs @@ -0,0 +1,21 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class FieldData : Data + { + public FieldData() { } + + public FieldData(FieldApi data) + { + Type = data.Type; + Name = data.Name; + Value = data.Value; + } + + public FieldType Type { get; set; } + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Core/Models/Data/FolderData.cs b/src/Core/Models/Data/FolderData.cs new file mode 100644 index 000000000..24a155a05 --- /dev/null +++ b/src/Core/Models/Data/FolderData.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Response; +using System; + +namespace Bit.Core.Models.Data +{ + public class FolderData : Data + { + public FolderData() { } + + public FolderData(FolderResponse response, string userId) + { + UserId = userId; + Id = response.Id; + Name = response.Name; + RevisionDate = response.RevisionDate; + } + + public string Id { get; set; } + public string UserId { get; set; } + public string Name { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Core/Models/Data/IdentityData.cs b/src/Core/Models/Data/IdentityData.cs new file mode 100644 index 000000000..ba98d8ae3 --- /dev/null +++ b/src/Core/Models/Data/IdentityData.cs @@ -0,0 +1,50 @@ +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class IdentityData : Data + { + public IdentityData() { } + + public IdentityData(IdentityApi data) + { + Title = data.Title; + FirstName = data.FirstName; + MiddleName = data.MiddleName; + LastName = data.LastName; + Address1 = data.Address1; + Address2 = data.Address2; + Address3 = data.Address3; + City = data.City; + State = data.State; + PostalCode = data.PostalCode; + Country = data.Country; + Company = data.Company; + Email = data.Email; + Phone = data.Phone; + SSN = data.SSN; + Username = data.Username; + PassportNumber = data.PassportNumber; + LicenseNumber = data.LicenseNumber; + } + + public string Title { get; set; } + public string FirstName { get; set; } + public string MiddleName { get; set; } + public string LastName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + } +} diff --git a/src/Core/Models/Data/LoginData.cs b/src/Core/Models/Data/LoginData.cs new file mode 100644 index 000000000..7afff7e36 --- /dev/null +++ b/src/Core/Models/Data/LoginData.cs @@ -0,0 +1,27 @@ +using Bit.Core.Models.Api; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Data +{ + public class LoginData : Data + { + public LoginData() { } + + public LoginData(LoginApi data) + { + Username = data.Username; + Password = data.Password; + PasswordRevisionDate = data.PasswordRevisionDate; + Totp = data.Totp; + Uris = data.Uris?.Select(u => new LoginUriData(u)).ToList(); + } + + public List Uris { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + } +} diff --git a/src/Core/Models/Data/LoginUriData.cs b/src/Core/Models/Data/LoginUriData.cs new file mode 100644 index 000000000..52294c523 --- /dev/null +++ b/src/Core/Models/Data/LoginUriData.cs @@ -0,0 +1,19 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class LoginUriData : Data + { + public LoginUriData() { } + + public LoginUriData(LoginUriApi data) + { + Uri = data.Uri; + Match = data.Match; + } + + public string Uri { get; set; } + public UriMatchType? Match { get; set; } + } +} diff --git a/src/Core/Models/Data/OrganizationData.cs b/src/Core/Models/Data/OrganizationData.cs new file mode 100644 index 000000000..4d1621fb2 --- /dev/null +++ b/src/Core/Models/Data/OrganizationData.cs @@ -0,0 +1,47 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Response; + +namespace Bit.Core.Models.Data +{ + public class OrganizationData : Data + { + public OrganizationData() { } + + public OrganizationData(ProfileOrganizationResponse response) + { + Id = response.Id; + Name = response.Name; + Status = response.Status; + Type = response.Type; + Enabled = response.Enabled; + UseGroups = response.UseGroups; + UseDirectory = response.UseDirectory; + UseEvents = response.UseEvents; + UseTotp = response.UseTotp; + Use2fa = response.Use2fa; + UseApi = response.UseApi; + SelfHost = response.SelfHost; + UsersGetPremium = response.UsersGetPremium; + Seats = response.Seats; + MaxCollections = response.MaxCollections; + MaxStorageGb = response.MaxStorageGb; + } + + public string Id { get; set; } + public string Name { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + public bool UseGroups { get; set; } + public bool UseDirectory { get; set; } + public bool UseEvents { get; set; } + public bool UseTotp { get; set; } + public bool Use2fa { get; set; } + public bool UseApi { get; set; } + public bool SelfHost { get; set; } + public bool UsersGetPremium { get; set; } + public int Seats { get; set; } + public int MaxCollections { get; set; } + public short? MaxStorageGb { get; set; } + } +} diff --git a/src/Core/Models/Data/PasswordHistoryData.cs b/src/Core/Models/Data/PasswordHistoryData.cs new file mode 100644 index 000000000..5d15bc0a7 --- /dev/null +++ b/src/Core/Models/Data/PasswordHistoryData.cs @@ -0,0 +1,19 @@ +using Bit.Core.Models.Response; +using System; + +namespace Bit.Core.Models.Data +{ + public class PasswordHistoryData : Data + { + public PasswordHistoryData() { } + + public PasswordHistoryData(PasswordHistoryResponse data) + { + Password = data.Password; + LastUsedDate = data.LastUsedDate; + } + + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/Core/Models/Data/SecureNoteData.cs b/src/Core/Models/Data/SecureNoteData.cs new file mode 100644 index 000000000..8b89bab7b --- /dev/null +++ b/src/Core/Models/Data/SecureNoteData.cs @@ -0,0 +1,17 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Core.Models.Data +{ + public class SecureNoteData : Data + { + public SecureNoteData() { } + + public SecureNoteData(SecureNoteApi data) + { + Type = data.Type; + } + + public SecureNoteType Type { get; set; } + } +} diff --git a/src/Core/Models/Domain/Attachment.cs b/src/Core/Models/Domain/Attachment.cs new file mode 100644 index 000000000..9b9e3704a --- /dev/null +++ b/src/Core/Models/Domain/Attachment.cs @@ -0,0 +1,68 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Attachment : Domain + { + private HashSet _map = new HashSet + { + "Id", + "Url", + "SizeName", + "FileName", + "Key" + }; + + public Attachment() { } + + public Attachment(AttachmentData obj, bool alreadyEncrypted = false) + { + Size = obj.Size; + BuildDomainModel(this, obj, _map, alreadyEncrypted, new HashSet { "Id", "Url", "SizeName" }); + } + + public string Id { get; set; } + public string Url { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public CipherString Key { get; set; } + public CipherString FileName { get; set; } + + public async Task DecryptAsync(string orgId) + { + var view = await DecryptObjAsync(new AttachmentView(this), this, new HashSet + { + "FileName" + }, orgId); + + if(Key != null) + { + var cryptoService = ServiceContainer.Resolve("cryptoService"); + try + { + var orgKey = await cryptoService.GetOrgKeyAsync(orgId); + var decValue = await cryptoService.DecryptToBytesAsync(Key, orgKey); + view.Key = new SymmetricCryptoKey(decValue); + } + catch + { + // TODO: error? + } + } + return view; + } + + public AttachmentData ToAttachmentData() + { + var a = new AttachmentData(); + a.Size = Size; + BuildDataModel(this, a, _map, new HashSet { "Id", "Url", "SizeName" }); + return a; + } + } +} diff --git a/src/Core/Models/Domain/AuthResult.cs b/src/Core/Models/Domain/AuthResult.cs new file mode 100644 index 000000000..1286bb0d5 --- /dev/null +++ b/src/Core/Models/Domain/AuthResult.cs @@ -0,0 +1,11 @@ +using Bit.Core.Enums; +using System.Collections.Generic; + +namespace Bit.Core.Models.Domain +{ + public class AuthResult + { + public bool TwoFactor { get; set; } + public Dictionary> TwoFactorProviders { get; set; } + } +} diff --git a/src/Core/Models/Domain/Card.cs b/src/Core/Models/Domain/Card.cs new file mode 100644 index 000000000..baeddea69 --- /dev/null +++ b/src/Core/Models/Domain/Card.cs @@ -0,0 +1,46 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Card : Domain + { + private HashSet _map = new HashSet + { + "CardholderName", + "Brand", + "Number", + "ExpMonth", + "ExpYear", + "Code" + }; + + public Card() { } + + public Card(CardData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public CipherString CardholderName { get; set; } + public CipherString Brand { get; set; } + public CipherString Number { get; set; } + public CipherString ExpMonth { get; set; } + public CipherString ExpYear { get; set; } + public CipherString Code { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new CardView(this), this, _map, orgId); + } + + public CardData ToCardData() + { + var c = new CardData(); + BuildDataModel(this, c, _map); + return c; + } + } +} diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs new file mode 100644 index 000000000..3d0e8ca20 --- /dev/null +++ b/src/Core/Models/Domain/Cipher.cs @@ -0,0 +1,194 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Cipher : Domain + { + public Cipher() { } + + public Cipher(CipherData obj, bool alreadyEncrypted = false, Dictionary localData = null) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "OrganizationId", + "FolderId", + "Name", + "Notes" + }, alreadyEncrypted, new HashSet { "Id", "OrganizationId", "FolderId" }); + + Type = obj.Type; + Favorite = obj.Favorite; + OrganizationUseTotp = obj.OrganizationUseTotp; + Edit = obj.Edit; + RevisionDate = obj.RevisionDate; + CollectionIds = obj.CollectionIds != null ? new HashSet(obj.CollectionIds) : null; + LocalData = localData; + + switch(Type) + { + case Enums.CipherType.Login: + Login = new Login(obj.Login, alreadyEncrypted); + break; + case Enums.CipherType.SecureNote: + SecureNote = new SecureNote(obj.SecureNote, alreadyEncrypted); + break; + case Enums.CipherType.Card: + Card = new Card(obj.Card, alreadyEncrypted); + break; + case Enums.CipherType.Identity: + Identity = new Identity(obj.Identity, alreadyEncrypted); + break; + default: + break; + } + + Attachments = obj.Attachments?.Select(a => new Attachment(a, alreadyEncrypted)).ToList(); + Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList(); + PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList(); + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public CipherString Name { get; set; } + public CipherString Notes { get; set; } + public Enums.CipherType Type { get; set; } + public bool Favorite { get; set; } + public bool OrganizationUseTotp { get; set; } + public bool Edit { get; set; } + public DateTime RevisionDate { get; set; } + public Dictionary LocalData { get; set; } + public Login Login { get; set; } + public Identity Identity { get; set; } + public Card Card { get; set; } + public SecureNote SecureNote { get; set; } + public List Attachments { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public HashSet CollectionIds { get; set; } + + public async Task DecryptAsync() + { + var model = new CipherView(this); + await DecryptObjAsync(model, this, new HashSet + { + "Name", + "Notes" + }, OrganizationId); + + switch(Type) + { + case Enums.CipherType.Login: + model.Login = await Login.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.SecureNote: + model.SecureNote = await SecureNote.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.Card: + model.Card = await Card.DecryptAsync(OrganizationId); + break; + case Enums.CipherType.Identity: + model.Identity = await Identity.DecryptAsync(OrganizationId); + break; + default: + break; + } + + if(Attachments?.Any() ?? false) + { + model.Attachments = new List(); + var tasks = new List(); + async Task decryptAndAddAttachmentAsync(Attachment attachment) + { + var decAttachment = await attachment.DecryptAsync(OrganizationId); + model.Attachments.Add(decAttachment); + } + foreach(var attachment in Attachments) + { + tasks.Add(decryptAndAddAttachmentAsync(attachment)); + } + await Task.WhenAll(tasks); + } + if(Fields?.Any() ?? false) + { + model.Fields = new List(); + var tasks = new List(); + async Task decryptAndAddFieldAsync(Field field) + { + var decField = await field.DecryptAsync(OrganizationId); + model.Fields.Add(decField); + } + foreach(var field in Fields) + { + tasks.Add(decryptAndAddFieldAsync(field)); + } + await Task.WhenAll(tasks); + } + if(PasswordHistory?.Any() ?? false) + { + model.PasswordHistory = new List(); + var tasks = new List(); + async Task decryptAndAddHistoryAsync(PasswordHistory ph) + { + var decPh = await ph.DecryptAsync(OrganizationId); + model.PasswordHistory.Add(decPh); + } + foreach(var ph in PasswordHistory) + { + tasks.Add(decryptAndAddHistoryAsync(ph)); + } + await Task.WhenAll(tasks); + } + return model; + } + + public CipherData ToCipherData(string userId) + { + var c = new CipherData + { + Id = Id, + OrganizationId = OrganizationId, + FolderId = FolderId, + UserId = OrganizationId != null ? userId : null, + Edit = Edit, + OrganizationUseTotp = OrganizationUseTotp, + Favorite = Favorite, + RevisionDate = RevisionDate, + Type = Type, + CollectionIds = CollectionIds.ToList() + }; + BuildDataModel(this, c, new HashSet + { + "Name", + "Notes" + }); + switch(c.Type) + { + case Enums.CipherType.Login: + c.Login = Login.ToLoginData(); + break; + case Enums.CipherType.SecureNote: + c.SecureNote = SecureNote.ToSecureNoteData(); + break; + case Enums.CipherType.Card: + c.Card = Card.ToCardData(); + break; + case Enums.CipherType.Identity: + c.Identity = Identity.ToIdentityData(); + break; + default: + break; + } + c.Fields = Fields?.Select(f => f.ToFieldData()).ToList(); + c.Attachments = Attachments?.Select(a => a.ToAttachmentData()).ToList(); + c.PasswordHistory = PasswordHistory?.Select(ph => ph.ToPasswordHistoryData()).ToList(); + return c; + } + } +} diff --git a/src/Core/Models/Domain/CipherString.cs b/src/Core/Models/Domain/CipherString.cs new file mode 100644 index 000000000..cb6c6e2ed --- /dev/null +++ b/src/Core/Models/Domain/CipherString.cs @@ -0,0 +1,122 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class CipherString + { + private string _decryptedValue; + + public CipherString(EncryptionType encryptionType, string data, string iv = null, string mac = null) + { + if(string.IsNullOrWhiteSpace(data)) + { + throw new ArgumentNullException(nameof(data)); + } + + if(!string.IsNullOrWhiteSpace(iv)) + { + EncryptedString = string.Format("{0}.{1}|{2}", (byte)encryptionType, iv, data); + } + else + { + EncryptedString = string.Format("{0}.{1}", (byte)encryptionType, data); + } + + if(!string.IsNullOrWhiteSpace(mac)) + { + EncryptedString = string.Format("{0}|{1}", EncryptedString, mac); + } + + EncryptionType = encryptionType; + Data = data; + Iv = iv; + Mac = mac; + } + + public CipherString(string encryptedString) + { + if(string.IsNullOrWhiteSpace(encryptedString)) + { + throw new ArgumentException(nameof(encryptedString)); + } + + EncryptedString = encryptedString; + var headerPieces = EncryptedString.Split('.'); + string[] encPieces; + + if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out EncryptionType encType)) + { + EncryptionType = encType; + encPieces = headerPieces[1].Split('|'); + } + else + { + encPieces = EncryptedString.Split('|'); + EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : + EncryptionType.AesCbc256_B64; + } + + switch(EncryptionType) + { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if(encPieces.Length != 3) + { + return; + } + Iv = encPieces[0]; + Data = encPieces[1]; + Mac = encPieces[2]; + break; + case EncryptionType.AesCbc256_B64: + if(encPieces.Length != 2) + { + return; + } + Iv = encPieces[0]; + Data = encPieces[1]; + break; + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if(encPieces.Length != 1) + { + return; + } + Data = encPieces[0]; + break; + default: + return; + } + } + + public EncryptionType EncryptionType { get; private set; } + public string EncryptedString { get; private set; } + public string Iv { get; private set; } + public string Data { get; private set; } + public string Mac { get; private set; } + + public async Task DecryptAsync(string orgId = null) + { + if(_decryptedValue != null) + { + return _decryptedValue; + } + + var cryptoService = ServiceContainer.Resolve("cryptoService"); + try + { + var orgKey = await cryptoService.GetOrgKeyAsync(orgId); + _decryptedValue = await cryptoService.DecryptToUtf8Async(this, orgKey); + } + catch + { + _decryptedValue = "[error: cannot decrypt]"; + } + return _decryptedValue; + } + } +} diff --git a/src/Core/Models/Domain/Collection.cs b/src/Core/Models/Domain/Collection.cs new file mode 100644 index 000000000..5e0578507 --- /dev/null +++ b/src/Core/Models/Domain/Collection.cs @@ -0,0 +1,41 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Collection : Domain + { + public Collection() { } + + public Collection(CollectionData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "OrganizationId", + "Name", + "ExternalId", + "ReadOnly" + }, alreadyEncrypted, new HashSet + { + "Id", + "OrganizationId", + "ExternalId", + "ReadOnly" + }); + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public CipherString Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + + public Task DecryptAsync() + { + return DecryptObjAsync(new CollectionView(this), this, new HashSet { "Name" }, OrganizationId); + } + } +} diff --git a/src/Core/Models/Domain/Domain.cs b/src/Core/Models/Domain/Domain.cs new file mode 100644 index 000000000..fc839f326 --- /dev/null +++ b/src/Core/Models/Domain/Domain.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public abstract class Domain + { + protected void BuildDomainModel(D domain, O dataObj, HashSet map, bool alreadyEncrypted, + HashSet notEncList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach(var prop in map) + { + var dataObjPropInfo = dataObjType.GetProperty(prop); + var dataObjProp = dataObjPropInfo.GetValue(dataObj); + var domainPropInfo = domainType.GetProperty(prop); + if(alreadyEncrypted || (notEncList?.Contains(prop) ?? false)) + { + domainPropInfo.SetValue(domain, dataObjProp, null); + } + else + { + domainPropInfo.SetValue(domain, + dataObjProp != null ? new CipherString(dataObjProp as string) : null, null); + } + } + } + + protected void BuildDataModel(D domain, O dataObj, HashSet map, + HashSet notCipherStringList = null) + where D : Domain + where O : Data.Data + { + var domainType = domain.GetType(); + var dataObjType = dataObj.GetType(); + foreach(var prop in map) + { + var domainPropInfo = domainType.GetProperty(prop); + var domainProp = domainPropInfo.GetValue(domain); + var dataObjPropInfo = dataObjType.GetProperty(prop); + if(notCipherStringList?.Contains(prop) ?? false) + { + dataObjPropInfo.SetValue(dataObj, domainProp, null); + } + else + { + dataObjPropInfo.SetValue(dataObj, (domainProp as CipherString)?.EncryptedString, null); + } + } + } + + protected async Task DecryptObjAsync(V viewModel, D domain, HashSet map, string orgId) + where V : View.View + { + var viewModelType = viewModel.GetType(); + var domainType = domain.GetType(); + + async Task decCsAndSetDec(string propName) + { + var domainPropInfo = domainType.GetProperty(propName); + string val = null; + if(domainPropInfo.GetValue(domain) is CipherString domainProp) + { + val = await domainProp.DecryptAsync(orgId); + } + var viewModelPropInfo = viewModelType.GetProperty(propName); + viewModelPropInfo.SetValue(viewModel, val, null); + }; + + var tasks = new List(); + foreach(var prop in map) + { + tasks.Add(decCsAndSetDec(prop)); + } + await Task.WhenAll(tasks); + return viewModel; + } + } +} diff --git a/src/Core/Models/Domain/EnvironmentUrls.cs b/src/Core/Models/Domain/EnvironmentUrls.cs new file mode 100644 index 000000000..6f6b43577 --- /dev/null +++ b/src/Core/Models/Domain/EnvironmentUrls.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Domain +{ + public class EnvironmentUrls + { + public string Base { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + } +} diff --git a/src/Core/Models/Domain/Field.cs b/src/Core/Models/Domain/Field.cs new file mode 100644 index 000000000..468241cd0 --- /dev/null +++ b/src/Core/Models/Domain/Field.cs @@ -0,0 +1,49 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Field : Domain + { + private HashSet _map = new HashSet + { + "Name", + "Value" + }; + + public Field() { } + + public Field(FieldData obj, bool alreadyEncrypted = false) + { + Type = obj.Type; + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public CipherString Name { get; set; } + public CipherString Value { get; set; } + public FieldType Type { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new FieldView(this), this, _map, orgId); + } + + public FieldData ToFieldData() + { + var f = new FieldData(); + BuildDataModel(this, f, new HashSet + { + "Name", + "Value", + "Type" + }, new HashSet + { + "Type" + }); + return f; + } + } +} diff --git a/src/Core/Models/Domain/Folder.cs b/src/Core/Models/Domain/Folder.cs new file mode 100644 index 000000000..bec956cbd --- /dev/null +++ b/src/Core/Models/Domain/Folder.cs @@ -0,0 +1,32 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Folder : Domain + { + public Folder() { } + + public Folder(FolderData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, new HashSet + { + "Id", + "Name" + }, alreadyEncrypted, new HashSet { "Id" }); + RevisionDate = obj.RevisionDate; + } + + public string Id { get; set; } + public CipherString Name { get; set; } + public DateTime RevisionDate { get; set; } + + public Task DecryptAsync() + { + return DecryptObjAsync(new FolderView(this), this, new HashSet { "Name" }, null); + } + } +} diff --git a/src/Core/Models/Domain/GeneratedPasswordHistory.cs b/src/Core/Models/Domain/GeneratedPasswordHistory.cs new file mode 100644 index 000000000..d4c176dac --- /dev/null +++ b/src/Core/Models/Domain/GeneratedPasswordHistory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Domain +{ + public class GeneratedPasswordHistory + { + public string Password { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/App/Abstractions/ITreeNodeObject.cs b/src/Core/Models/Domain/ITreeNodeObject.cs similarity index 76% rename from src/App/Abstractions/ITreeNodeObject.cs rename to src/Core/Models/Domain/ITreeNodeObject.cs index 8c29a67f6..f91c89151 100644 --- a/src/App/Abstractions/ITreeNodeObject.cs +++ b/src/Core/Models/Domain/ITreeNodeObject.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Abstractions +namespace Bit.Core.Models.Domain { public interface ITreeNodeObject { diff --git a/src/Core/Models/Domain/Identity.cs b/src/Core/Models/Domain/Identity.cs new file mode 100644 index 000000000..c45bb64d8 --- /dev/null +++ b/src/Core/Models/Domain/Identity.cs @@ -0,0 +1,70 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Identity : Domain + { + private HashSet _map = new HashSet + { + "Title", + "FirstName", + "MiddleName", + "LastName", + "Address1", + "Address2", + "Address3", + "City", + "State", + "PostalCode", + "Country", + "Company", + "Email", + "Phone", + "SSN", + "Username", + "PassportNumber", + "LicenseNumber" + }; + + public Identity() { } + + public Identity(IdentityData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public CipherString Title { get; set; } + public CipherString FirstName { get; set; } + public CipherString MiddleName { get; set; } + public CipherString LastName { get; set; } + public CipherString Address1 { get; set; } + public CipherString Address2 { get; set; } + public CipherString Address3 { get; set; } + public CipherString City { get; set; } + public CipherString State { get; set; } + public CipherString PostalCode { get; set; } + public CipherString Country { get; set; } + public CipherString Company { get; set; } + public CipherString Email { get; set; } + public CipherString Phone { get; set; } + public CipherString SSN { get; set; } + public CipherString Username { get; set; } + public CipherString PassportNumber { get; set; } + public CipherString LicenseNumber { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new IdentityView(this), this, _map, orgId); + } + + public IdentityData ToIdentityData() + { + var i = new IdentityData(); + BuildDataModel(this, i, _map); + return i; + } + } +} diff --git a/src/Core/Models/Domain/Login.cs b/src/Core/Models/Domain/Login.cs new file mode 100644 index 000000000..adb2314a4 --- /dev/null +++ b/src/Core/Models/Domain/Login.cs @@ -0,0 +1,68 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class Login : Domain + { + public Login() { } + + public Login(LoginData obj, bool alreadyEncrypted = false) + { + PasswordRevisionDate = obj.PasswordRevisionDate; + Uris = obj.Uris?.Select(u => new LoginUri(u, alreadyEncrypted)).ToList(); + BuildDomainModel(this, obj, new HashSet + { + "Username", + "Password", + "Totp" + }, alreadyEncrypted); + } + + public List Uris { get; set; } + public CipherString Username { get; set; } + public CipherString Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public CipherString Totp { get; set; } + + public async Task DecryptAsync(string orgId) + { + var view = await DecryptObjAsync(new LoginView(this), this, new HashSet + { + "Username", + "Password", + "Totp" + }, orgId); + if(Uris != null) + { + view.Uris = new List(); + foreach(var uri in Uris) + { + view.Uris.Add(await uri.DecryptAsync(orgId)); + } + } + return view; + } + + public LoginData ToLoginData() + { + var l = new LoginData(); + l.PasswordRevisionDate = PasswordRevisionDate; + BuildDataModel(this, l, new HashSet + { + "Username", + "Password", + "Totp" + }); + if(Uris?.Any() ?? false) + { + l.Uris = Uris.Select(u => u.ToLoginUriData()).ToList(); + } + return l; + } + } +} diff --git a/src/Core/Models/Domain/LoginUri.cs b/src/Core/Models/Domain/LoginUri.cs new file mode 100644 index 000000000..d6467e4a7 --- /dev/null +++ b/src/Core/Models/Domain/LoginUri.cs @@ -0,0 +1,39 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class LoginUri : Domain + { + private HashSet _map = new HashSet + { + "Uri" + }; + + public LoginUri() { } + + public LoginUri(LoginUriData obj, bool alreadyEncrypted = false) + { + Match = obj.Match; + BuildDomainModel(this, obj, _map, alreadyEncrypted); + } + + public CipherString Uri { get; set; } + public UriMatchType? Match { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new LoginUriView(this), this, _map, orgId); + } + + public LoginUriData ToLoginUriData() + { + var u = new LoginUriData(); + BuildDataModel(this, u, _map, new HashSet { "Match" }); + return u; + } + } +} diff --git a/src/Core/Models/Domain/Message.cs b/src/Core/Models/Domain/Message.cs new file mode 100644 index 000000000..1553cbf72 --- /dev/null +++ b/src/Core/Models/Domain/Message.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Domain +{ + public class Message + { + public string Command { get; set; } + public object Data { get; set; } + } +} diff --git a/src/Core/Models/Domain/Organization.cs b/src/Core/Models/Domain/Organization.cs new file mode 100644 index 000000000..cb2857605 --- /dev/null +++ b/src/Core/Models/Domain/Organization.cs @@ -0,0 +1,78 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Organization + { + public Organization() { } + + public Organization(OrganizationData obj) + { + Id = obj.Id; + Name = obj.Name; + Status = obj.Status; + Type = obj.Type; + Enabled = obj.Enabled; + UseGroups = obj.UseGroups; + UseDirectory = obj.UseDirectory; + UseEvents = obj.UseEvents; + UseTotp = obj.UseTotp; + Use2fa = obj.Use2fa; + UseApi = obj.UseApi; + SelfHost = obj.SelfHost; + UsersGetPremium = obj.UsersGetPremium; + Seats = obj.Seats; + MaxCollections = obj.MaxCollections; + MaxStorageGb = obj.MaxStorageGb; + } + + public string Id { get; set; } + public string Name { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public bool Enabled { get; set; } + public bool UseGroups { get; set; } + public bool UseDirectory { get; set; } + public bool UseEvents { get; set; } + public bool UseTotp { get; set; } + public bool Use2fa { get; set; } + public bool UseApi { get; set; } + public bool SelfHost { get; set; } + public bool UsersGetPremium { get; set; } + public int Seats { get; set; } + public int MaxCollections { get; set; } + public short? MaxStorageGb { get; set; } + + public bool CanAccess + { + get + { + if(Type == OrganizationUserType.Owner) + { + return true; + } + return Enabled && Status == OrganizationUserStatusType.Confirmed; + } + } + + public bool IsManager + { + get + { + switch(Type) + { + case OrganizationUserType.Owner: + case OrganizationUserType.Admin: + case OrganizationUserType.Manager: + return true; + default: + return false; + } + } + } + + public bool IsAdmin => Type == OrganizationUserType.Owner || Type == OrganizationUserType.Admin; + public bool IsOwner => Type == OrganizationUserType.Owner; + } +} diff --git a/src/Core/Models/Domain/PasswordGenerationOptions.cs b/src/Core/Models/Domain/PasswordGenerationOptions.cs new file mode 100644 index 000000000..605ff0040 --- /dev/null +++ b/src/Core/Models/Domain/PasswordGenerationOptions.cs @@ -0,0 +1,58 @@ +namespace Bit.Core.Models.Domain +{ + public class PasswordGenerationOptions + { + public PasswordGenerationOptions() { } + + public PasswordGenerationOptions(bool defaultOptions) + { + if(defaultOptions) + { + Length = 14; + Ambiguous = false; + Number = true; + MinNumber = 1; + Uppercase = true; + MinUppercase = 0; + Lowercase = true; + MinLowercase = 0; + Special = false; + MinSpecial = 1; + Type = "password"; + NumWords = 3; + WordSeparator = "-"; + } + } + + public int? Length { get; set; } + public bool? Ambiguous { get; set; } + public bool? Number { get; set; } + public int? MinNumber { get; set; } + public bool? Uppercase { get; set; } + public int? MinUppercase { get; set; } + public bool? Lowercase { get; set; } + public int? MinLowercase { get; set; } + public bool? Special { get; set; } + public int? MinSpecial { get; set; } + public string Type { get; set; } + public int? NumWords { get; set; } + public string WordSeparator { get; set; } + + public void Merge(PasswordGenerationOptions defaults) + { + Length = Length ?? defaults.Length; + Ambiguous = Ambiguous ?? defaults.Ambiguous; + Number = Number ?? defaults.Number; + MinNumber = MinNumber ?? defaults.MinNumber; + Uppercase = Uppercase ?? defaults.Uppercase; + MinUppercase = MinUppercase ?? defaults.MinUppercase; + Lowercase = Lowercase ?? defaults.Lowercase; + MinLowercase = MinLowercase ?? defaults.MinLowercase; + Special = Special ?? defaults.Special; + MinSpecial = MinSpecial ?? defaults.MinSpecial; + Type = Type ?? defaults.Type; + NumWords = NumWords ?? defaults.NumWords; + WordSeparator = WordSeparator ?? defaults.WordSeparator; + } + } +} diff --git a/src/Core/Models/Domain/PasswordHistory.cs b/src/Core/Models/Domain/PasswordHistory.cs new file mode 100644 index 000000000..6ce9bc288 --- /dev/null +++ b/src/Core/Models/Domain/PasswordHistory.cs @@ -0,0 +1,40 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class PasswordHistory : Domain + { + private HashSet _map = new HashSet + { + "Password" + }; + + public PasswordHistory() { } + + public PasswordHistory(PasswordHistoryData obj, bool alreadyEncrypted = false) + { + BuildDomainModel(this, obj, _map, alreadyEncrypted); + LastUsedDate = obj.LastUsedDate.GetValueOrDefault(); + } + + public CipherString Password { get; set; } + public DateTime LastUsedDate { get; set; } + + public Task DecryptAsync(string orgId) + { + return DecryptObjAsync(new PasswordHistoryView(this), this, _map, orgId); + } + + public PasswordHistoryData ToPasswordHistoryData() + { + var ph = new PasswordHistoryData(); + ph.LastUsedDate = LastUsedDate; + BuildDataModel(this, ph, _map); + return ph; + } + } +} diff --git a/src/Core/Models/Domain/SecureNote.cs b/src/Core/Models/Domain/SecureNote.cs new file mode 100644 index 000000000..6d3afab4c --- /dev/null +++ b/src/Core/Models/Domain/SecureNote.cs @@ -0,0 +1,32 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.View; +using System.Threading.Tasks; + +namespace Bit.Core.Models.Domain +{ + public class SecureNote : Domain + { + public SecureNote() { } + + public SecureNote(SecureNoteData obj, bool alreadyEncrypted = false) + { + Type = obj.Type; + } + + public SecureNoteType Type { get; set; } + + public Task DecryptAsync(string orgId) + { + return Task.FromResult(new SecureNoteView(this)); + } + + public SecureNoteData ToSecureNoteData() + { + return new SecureNoteData + { + Type = Type + }; + } + } +} diff --git a/src/Core/Models/Domain/SymmetricCryptoKey.cs b/src/Core/Models/Domain/SymmetricCryptoKey.cs new file mode 100644 index 000000000..9f2439b79 --- /dev/null +++ b/src/Core/Models/Domain/SymmetricCryptoKey.cs @@ -0,0 +1,77 @@ +using Bit.Core.Enums; +using System; +using System.Linq; + +namespace Bit.Core.Models.Domain +{ + public class SymmetricCryptoKey + { + public SymmetricCryptoKey(byte[] key, EncryptionType? encType = null) + { + if(key == null) + { + throw new Exception("Must provide key."); + } + + if(encType == null) + { + if(key.Length == 32) + { + encType = EncryptionType.AesCbc256_B64; + } + else if(key.Length == 64) + { + encType = EncryptionType.AesCbc256_HmacSha256_B64; + } + else + { + throw new Exception("Unable to determine encType."); + } + } + + Key = key; + EncType = encType.Value; + + if(EncType == EncryptionType.AesCbc256_B64 && Key.Length == 32) + { + EncKey = Key; + MacKey = null; + } + else if(EncType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32) + { + EncKey = new ArraySegment(Key, 0, 16).ToArray(); + MacKey = new ArraySegment(Key, 16, 16).ToArray(); + } + else if(EncType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64) + { + EncKey = new ArraySegment(Key, 0, 32).ToArray(); + MacKey = new ArraySegment(Key, 32, 32).ToArray(); + } + else + { + throw new Exception("Unsupported encType/key length."); + } + + if(Key != null) + { + KeyB64 = Convert.ToBase64String(Key); + } + if(EncKey != null) + { + EncKeyB64 = Convert.ToBase64String(EncKey); + } + if(MacKey != null) + { + MacKeyB64 = Convert.ToBase64String(MacKey); + } + } + + public byte[] Key { get; set; } + public byte[] EncKey { get; set; } + public byte[] MacKey { get; set; } + public EncryptionType EncType { get; set; } + public string KeyB64 { get; set; } + public string EncKeyB64 { get; set; } + public string MacKeyB64 { get; set; } + } +} diff --git a/src/App/Models/TreeNode.cs b/src/Core/Models/Domain/TreeNode.cs similarity index 81% rename from src/App/Models/TreeNode.cs rename to src/Core/Models/Domain/TreeNode.cs index e06e2f41b..524b81e63 100644 --- a/src/App/Models/TreeNode.cs +++ b/src/Core/Models/Domain/TreeNode.cs @@ -1,19 +1,18 @@ -using Bit.App.Abstractions; -using System.Collections.Generic; +using System.Collections.Generic; -namespace Bit.App.Models +namespace Bit.Core.Models.Domain { public class TreeNode where T : ITreeNodeObject { + public T Parent { get; set; } + public T Node { get; set; } + public List> Children { get; set; } = new List>(); + public TreeNode(T node, string name, T parent) { Parent = parent; Node = node; Node.Name = name; } - - public T Parent { get; set; } - public T Node { get; set; } - public List> Children { get; set; } = new List>(); } } diff --git a/src/Core/Models/Domain/TwoFactorProvider.cs b/src/Core/Models/Domain/TwoFactorProvider.cs new file mode 100644 index 000000000..be3489a08 --- /dev/null +++ b/src/Core/Models/Domain/TwoFactorProvider.cs @@ -0,0 +1,14 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class TwoFactorProvider + { + public TwoFactorProviderType Type { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int Priority { get; set; } + public int Sort { get; set; } + public bool Premium { get; set; } + } +} diff --git a/src/Core/Models/Request/AttachmentRequest.cs b/src/Core/Models/Request/AttachmentRequest.cs new file mode 100644 index 000000000..96837545f --- /dev/null +++ b/src/Core/Models/Request/AttachmentRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Request +{ + public class AttachmentRequest + { + public string FileName { get; set; } + public string Key { get; set; } + } +} diff --git a/src/Core/Models/Request/CipherCollectionsRequest.cs b/src/Core/Models/Request/CipherCollectionsRequest.cs new file mode 100644 index 000000000..f5659ef73 --- /dev/null +++ b/src/Core/Models/Request/CipherCollectionsRequest.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Request +{ + public class CipherCollectionsRequest + { + public CipherCollectionsRequest(List collectionIds) + { + CollectionIds = collectionIds ?? new List(); + } + + public List CollectionIds { get; set; } + } +} diff --git a/src/Core/Models/Request/CipherCreateRequest.cs b/src/Core/Models/Request/CipherCreateRequest.cs new file mode 100644 index 000000000..aefb9b340 --- /dev/null +++ b/src/Core/Models/Request/CipherCreateRequest.cs @@ -0,0 +1,18 @@ +using Bit.Core.Models.Domain; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Request +{ + public class CipherCreateRequest + { + public CipherCreateRequest(Cipher cipher) + { + Cipher = new CipherRequest(cipher); + CollectionIds = cipher.CollectionIds?.ToList(); + } + + public CipherRequest Cipher { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Core/Models/Request/CipherRequest.cs b/src/Core/Models/Request/CipherRequest.cs new file mode 100644 index 000000000..49fba9c9a --- /dev/null +++ b/src/Core/Models/Request/CipherRequest.cs @@ -0,0 +1,122 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Models.Domain; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Request +{ + public class CipherRequest + { + public CipherRequest(Cipher cipher) + { + Type = cipher.Type; + OrganizationId = cipher.OrganizationId; + FolderId = cipher.FolderId; + Name = cipher.Name?.EncryptedString; + Notes = cipher.Notes?.EncryptedString; + Favorite = cipher.Favorite; + + switch(Type) + { + case CipherType.Login: + Login = new LoginApi + { + Uris = cipher.Login.Uris?.Select( + u => new LoginUriApi { Match = u.Match, Uri = u.Uri?.EncryptedString }).ToList(), + Username = cipher.Login.Username?.EncryptedString, + Password = cipher.Login.Password?.EncryptedString, + PasswordRevisionDate = cipher.Login.PasswordRevisionDate, + Totp = cipher.Login.Totp?.EncryptedString + }; + break; + case CipherType.Card: + Card = new CardApi + { + CardholderName = cipher.Card.CardholderName?.EncryptedString, + Brand = cipher.Card.Brand?.EncryptedString, + Number = cipher.Card.Number?.EncryptedString, + ExpMonth = cipher.Card.ExpMonth?.EncryptedString, + ExpYear = cipher.Card.ExpYear?.EncryptedString, + Code = cipher.Card.Code?.EncryptedString + }; + break; + case CipherType.Identity: + Identity = new IdentityApi + { + Title = cipher.Identity.Title?.EncryptedString, + FirstName = cipher.Identity.FirstName?.EncryptedString, + MiddleName = cipher.Identity.MiddleName?.EncryptedString, + LastName = cipher.Identity.LastName?.EncryptedString, + Address1 = cipher.Identity.Address1?.EncryptedString, + Address2 = cipher.Identity.Address2?.EncryptedString, + Address3 = cipher.Identity.Address3?.EncryptedString, + City = cipher.Identity.City?.EncryptedString, + State = cipher.Identity.State?.EncryptedString, + PostalCode = cipher.Identity.PostalCode?.EncryptedString, + Country = cipher.Identity.Country?.EncryptedString, + Company = cipher.Identity.Company?.EncryptedString, + Email = cipher.Identity.Email?.EncryptedString, + Phone = cipher.Identity.Phone?.EncryptedString, + SSN = cipher.Identity.SSN?.EncryptedString, + Username = cipher.Identity.Username?.EncryptedString, + PassportNumber = cipher.Identity.PassportNumber?.EncryptedString, + LicenseNumber = cipher.Identity.LicenseNumber?.EncryptedString + }; + break; + case CipherType.SecureNote: + SecureNote = new SecureNoteApi + { + Type = cipher.SecureNote.Type + }; + break; + default: + break; + } + + Fields = cipher.Fields?.Select(f => new FieldApi + { + Type = f.Type, + Name = f.Name?.EncryptedString, + Value = f.Value?.EncryptedString + }).ToList(); + + PasswordHistory = cipher.PasswordHistory?.Select(ph => new PasswordHistoryRequest + { + Password = ph.Password?.EncryptedString, + LastUsedDate = ph.LastUsedDate + }).ToList(); + + if(cipher.Attachments != null) + { + Attachments = new Dictionary(); + Attachments2 = new Dictionary(); + foreach(var attachment in cipher.Attachments) + { + var fileName = attachment.FileName?.EncryptedString; + Attachments.Add(attachment.Id, fileName); + Attachments2.Add(attachment.Id, new AttachmentRequest + { + FileName = fileName, + Key = attachment.Key?.EncryptedString + }); + } + } + } + + public CipherType Type { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public bool Favorite { get; set; } + public LoginApi Login { get; set; } + public SecureNoteApi SecureNote { get; set; } + public CardApi Card { get; set; } + public IdentityApi Identity { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public Dictionary Attachments { get; set; } + public Dictionary Attachments2 { get; set; } + } +} diff --git a/src/Core/Models/Request/CipherShareRequest.cs b/src/Core/Models/Request/CipherShareRequest.cs new file mode 100644 index 000000000..4fb4f3761 --- /dev/null +++ b/src/Core/Models/Request/CipherShareRequest.cs @@ -0,0 +1,18 @@ +using Bit.Core.Models.Domain; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Request +{ + public class CipherShareRequest + { + public CipherShareRequest(Cipher cipher) + { + Cipher = new CipherRequest(cipher); + CollectionIds = cipher.CollectionIds?.ToList(); + } + + public CipherRequest Cipher { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Core/Models/Request/DeviceRequest.cs b/src/Core/Models/Request/DeviceRequest.cs new file mode 100644 index 000000000..ecc9bf90c --- /dev/null +++ b/src/Core/Models/Request/DeviceRequest.cs @@ -0,0 +1,21 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class DeviceRequest + { + public DeviceRequest(string appId, IPlatformUtilsService platformUtilsService) + { + Type = platformUtilsService.GetDevice(); + Name = platformUtilsService.GetDeviceString(); + Identifier = appId; + PushToken = null; // TODO? + } + + public DeviceType? Type { get; set; } + public string Name { get; set; } + public string Identifier { get; set; } + public string PushToken { get; set; } + } +} diff --git a/src/Core/Models/Request/DeviceTokenRequest.cs b/src/Core/Models/Request/DeviceTokenRequest.cs new file mode 100644 index 000000000..8806f3b0a --- /dev/null +++ b/src/Core/Models/Request/DeviceTokenRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class DeviceTokenRequest + { + public string PushToken { get; set; } + } +} diff --git a/src/App/Models/Api/Request/FolderRequest.cs b/src/Core/Models/Request/FolderRequest.cs similarity index 74% rename from src/App/Models/Api/Request/FolderRequest.cs rename to src/Core/Models/Request/FolderRequest.cs index 9d90e9ef6..a09b5134e 100644 --- a/src/App/Models/Api/Request/FolderRequest.cs +++ b/src/Core/Models/Request/FolderRequest.cs @@ -1,4 +1,6 @@ -namespace Bit.App.Models.Api +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.Request { public class FolderRequest { diff --git a/src/Core/Models/Request/KeysRequest.cs b/src/Core/Models/Request/KeysRequest.cs new file mode 100644 index 000000000..9cae0a89d --- /dev/null +++ b/src/Core/Models/Request/KeysRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Request +{ + public class KeysRequest + { + public string PublicKey { get; set; } + public string EncryptedPrivateKey { get; set; } + } +} diff --git a/src/App/Models/Api/Request/PasswordHintRequest.cs b/src/Core/Models/Request/PasswordHintRequest.cs similarity index 71% rename from src/App/Models/Api/Request/PasswordHintRequest.cs rename to src/Core/Models/Request/PasswordHintRequest.cs index 6d515f9ad..bd3389abb 100644 --- a/src/App/Models/Api/Request/PasswordHintRequest.cs +++ b/src/Core/Models/Request/PasswordHintRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Request { public class PasswordHintRequest { diff --git a/src/Core/Models/Request/PasswordHistoryRequest.cs b/src/Core/Models/Request/PasswordHistoryRequest.cs new file mode 100644 index 000000000..33e323890 --- /dev/null +++ b/src/Core/Models/Request/PasswordHistoryRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Request +{ + public class PasswordHistoryRequest + { + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/App/Models/Api/Request/PreloginRequest.cs b/src/Core/Models/Request/PreloginRequest.cs similarity index 71% rename from src/App/Models/Api/Request/PreloginRequest.cs rename to src/Core/Models/Request/PreloginRequest.cs index 1cecc66bb..13097a5b9 100644 --- a/src/App/Models/Api/Request/PreloginRequest.cs +++ b/src/Core/Models/Request/PreloginRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Request { public class PreloginRequest { diff --git a/src/Core/Models/Request/RegisterRequest.cs b/src/Core/Models/Request/RegisterRequest.cs new file mode 100644 index 000000000..959b1d988 --- /dev/null +++ b/src/Core/Models/Request/RegisterRequest.cs @@ -0,0 +1,19 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class RegisterRequest + { + public string Name { get; set; } + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string MasterPasswordHint { get; set; } + public string Key { get; set; } + public KeysRequest Keys { get; set; } + public string Token { get; set; } + public Guid? OrganizationUserId { get; set; } + public KdfType? Kdf { get; set; } + public int? KdfIterations { get; set; } + } +} diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs new file mode 100644 index 000000000..7a51f9e79 --- /dev/null +++ b/src/Core/Models/Request/TokenRequest.cs @@ -0,0 +1,44 @@ +using Bit.Core.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Request +{ + public class TokenRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string Token { get; set; } + public TwoFactorProviderType? Provider { get; set; } + public bool Remember { get; set; } + public DeviceRequest Device { get; set; } + + public Dictionary ToIdentityToken(string clientId) + { + var obj = new Dictionary + { + ["grant_type"] = "password", + ["username"] = Email, + ["password"] = MasterPasswordHash, + ["scope"] = "api offline_access", + ["client_id"] = clientId + }; + if(Device != null) + { + obj.Add("deviceType", ((int)Device.Type).ToString()); + obj.Add("deviceIdentifier", Device.Identifier); + obj.Add("deviceName", Device.Name); + // TODO + // dict.Add("devicePushToken", null); + } + if(!string.IsNullOrWhiteSpace(Token) && Provider != null) + { + obj.Add("twoFactorToken", Token); + obj.Add("twoFactorProvider", ((int)Provider.Value).ToString()); + obj.Add("twoFactorRemember", Remember ? "1" : "0"); + } + return obj; + } + } +} diff --git a/src/App/Models/Api/Request/TwoFactorEmailRequest.cs b/src/Core/Models/Request/TwoFactorEmailRequest.cs similarity index 80% rename from src/App/Models/Api/Request/TwoFactorEmailRequest.cs rename to src/Core/Models/Request/TwoFactorEmailRequest.cs index 5d834b6bc..3446df586 100644 --- a/src/App/Models/Api/Request/TwoFactorEmailRequest.cs +++ b/src/Core/Models/Request/TwoFactorEmailRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Request { public class TwoFactorEmailRequest { diff --git a/src/App/Models/Api/Response/AttachmentResponse.cs b/src/Core/Models/Response/AttachmentResponse.cs similarity index 88% rename from src/App/Models/Api/Response/AttachmentResponse.cs rename to src/Core/Models/Response/AttachmentResponse.cs index fe1f0baa4..1dea2a877 100644 --- a/src/App/Models/Api/Response/AttachmentResponse.cs +++ b/src/Core/Models/Response/AttachmentResponse.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { public class AttachmentResponse { diff --git a/src/Core/Models/Response/BreachAccountResponse.cs b/src/Core/Models/Response/BreachAccountResponse.cs new file mode 100644 index 000000000..f32ba923a --- /dev/null +++ b/src/Core/Models/Response/BreachAccountResponse.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class BreachAccountResponse + { + public string AddedDate { get; set; } + public string BreachDate { get; set; } + public List DataClasses { get; set; } + public string Description { get; set; } + public string Domain { get; set; } + public bool IsActive { get; set; } + public bool IsVerified { get; set; } + public string LogoPath { get; set; } + public string ModifiedDate { get; set; } + public string Name { get; set; } + public int PwnCount { get; set; } + public string Title { get; set; } + } +} diff --git a/src/Core/Models/Response/CipherResponse.cs b/src/Core/Models/Response/CipherResponse.cs new file mode 100644 index 000000000..a78c0ec1e --- /dev/null +++ b/src/Core/Models/Response/CipherResponse.cs @@ -0,0 +1,28 @@ +using Bit.Core.Models.Api; +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class CipherResponse + { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public Enums.CipherType Type { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public List Fields { get; set; } + public LoginApi Login { get; set; } + public CardApi Card { get; set; } + public IdentityApi Identity { get; set; } + public SecureNoteApi SecureNote { get; set; } + public bool Favorite { get; set; } + public bool Edit { get; set; } + public bool OrganizationUseTotp { get; set; } + public DateTime RevisionDate { get; set; } + public List Attachments { get; set; } + public List PasswordHistory { get; set; } + public List CollectionIds { get; set; } + } +} diff --git a/src/Core/Models/Response/CollectionResponse.cs b/src/Core/Models/Response/CollectionResponse.cs new file mode 100644 index 000000000..7895ecb7f --- /dev/null +++ b/src/Core/Models/Response/CollectionResponse.cs @@ -0,0 +1,15 @@ +namespace Bit.Core.Models.Response +{ + public class CollectionResponse + { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + } + + public class CollectionDetailsResponse : CollectionResponse + { + public bool ReadOnly { get; set; } + } +} diff --git a/src/Core/Models/Response/DomainsResponse.cs b/src/Core/Models/Response/DomainsResponse.cs new file mode 100644 index 000000000..ecdd0909c --- /dev/null +++ b/src/Core/Models/Response/DomainsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class DomainsResponse + { + public List> EquivalentDomains { get; set; } + public List GlobalEquivalentDomains { get; set; } = new List(); + } +} diff --git a/src/Core/Models/Response/ErrorResponse.cs b/src/Core/Models/Response/ErrorResponse.cs new file mode 100644 index 000000000..b8e1cafdf --- /dev/null +++ b/src/Core/Models/Response/ErrorResponse.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Bit.Core.Models.Response +{ + public class ErrorResponse + { + public ErrorResponse() { } + + public ErrorResponse(JObject response, HttpStatusCode status, bool identityResponse = false) + { + JObject errorModel = null; + if(response != null) + { + var responseErrorModel = response.GetValue("ErrorModel", StringComparison.OrdinalIgnoreCase); + if(responseErrorModel != null && identityResponse) + { + errorModel = responseErrorModel.Value(); ; + } + else + { + errorModel = response; + } + } + if(errorModel != null) + { + Message = errorModel.GetValue("Message", StringComparison.OrdinalIgnoreCase)?.Value(); + ValidationErrors = errorModel.GetValue("ValidationErrors", StringComparison.OrdinalIgnoreCase) + ?.Value>>(); + } + else + { + if((int)status == 429) + { + Message = "Rate limit exceeded. Try again later."; + } + } + StatusCode = status; + } + + public string Message { get; set; } + public Dictionary> ValidationErrors { get; set; } + public HttpStatusCode StatusCode { get; set; } + + public string GetSingleMessage() + { + if(ValidationErrors == null) + { + return Message; + } + foreach(var error in ValidationErrors) + { + if(error.Value?.Any() ?? false) + { + return error.Value[0]; + } + } + return Message; + } + } +} diff --git a/src/App/Models/Api/Response/FolderResponse.cs b/src/Core/Models/Response/FolderResponse.cs similarity index 84% rename from src/App/Models/Api/Response/FolderResponse.cs rename to src/Core/Models/Response/FolderResponse.cs index 869a476f5..e62fda4fb 100644 --- a/src/App/Models/Api/Response/FolderResponse.cs +++ b/src/Core/Models/Response/FolderResponse.cs @@ -1,6 +1,6 @@ using System; -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { public class FolderResponse { diff --git a/src/Core/Models/Response/GlobalDomainResponse.cs b/src/Core/Models/Response/GlobalDomainResponse.cs new file mode 100644 index 000000000..354a9cf4b --- /dev/null +++ b/src/Core/Models/Response/GlobalDomainResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class GlobalDomainResponse + { + public int Type { get; set; } + public List Domains { get; set; } + public bool Excluded { get; set; } + } +} diff --git a/src/App/Models/Api/Response/TokenResponse.cs b/src/Core/Models/Response/IdentityTokenResponse.cs similarity index 60% rename from src/App/Models/Api/Response/TokenResponse.cs rename to src/Core/Models/Response/IdentityTokenResponse.cs index 84d553a9d..e55d03b53 100644 --- a/src/App/Models/Api/Response/TokenResponse.cs +++ b/src/Core/Models/Response/IdentityTokenResponse.cs @@ -1,22 +1,19 @@ -using Bit.App.Enums; -using Newtonsoft.Json; -using System.Collections.Generic; +using Newtonsoft.Json; -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { - public class TokenResponse + public class IdentityTokenResponse { [JsonProperty("access_token")] public string AccessToken { get; set; } [JsonProperty("expires_in")] - public long ExpiresIn { get; set; } + public string ExpiresIn { get; set; } [JsonProperty("refresh_token")] public string RefreshToken { get; set; } [JsonProperty("token_type")] public string TokenType { get; set; } - public Dictionary> TwoFactorProviders2 { get; set; } public string PrivateKey { get; set; } - public string TwoFactorToken { get; set; } public string Key { get; set; } + public string TwoFactorToken { get; set; } } } diff --git a/src/Core/Models/Response/IdentityTwoFactorResponse.cs b/src/Core/Models/Response/IdentityTwoFactorResponse.cs new file mode 100644 index 000000000..82f082e9e --- /dev/null +++ b/src/Core/Models/Response/IdentityTwoFactorResponse.cs @@ -0,0 +1,11 @@ +using Bit.Core.Enums; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class IdentityTwoFactorResponse + { + public List TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProviders2 { get; set; } + } +} diff --git a/src/App/Models/PushNotification.cs b/src/Core/Models/Response/NotificationResponse.cs similarity index 55% rename from src/App/Models/PushNotification.cs rename to src/Core/Models/Response/NotificationResponse.cs index 40b587c5d..8e37b2673 100644 --- a/src/App/Models/PushNotification.cs +++ b/src/Core/Models/Response/NotificationResponse.cs @@ -1,36 +1,34 @@ -using System; +using Bit.Core.Enums; +using System; using System.Collections.Generic; -using Bit.App.Enums; -namespace Bit.App.Models +namespace Bit.Core.Models.Response { - public class PushNotificationData - { - public PushType Type { get; set; } - } - - public class PushNotificationDataPayload : PushNotificationData + public class NotificationResponse { + public string ContextId { get; set; } + public NotificationType Type { get; set; } public string Payload { get; set; } + public object PayloadObject { get; set; } } - public class SyncCipherPushNotification + public class SyncCipherNotification { public string Id { get; set; } public string UserId { get; set; } public string OrganizationId { get; set; } - public List CollectionIds { get; set; } + public HashSet CollectionIds { get; set; } public DateTime RevisionDate { get; set; } } - public class SyncFolderPushNotification + public class SyncFolderNotification { public string Id { get; set; } public string UserId { get; set; } public DateTime RevisionDate { get; set; } } - public class UserPushNotification + public class UserNotification { public string UserId { get; set; } public DateTime Date { get; set; } diff --git a/src/Core/Models/Response/PasswordHistoryResponse.cs b/src/Core/Models/Response/PasswordHistoryResponse.cs new file mode 100644 index 000000000..2903ede72 --- /dev/null +++ b/src/Core/Models/Response/PasswordHistoryResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Response +{ + public class PasswordHistoryResponse + { + public string Password { get; set; } + public DateTime? LastUsedDate { get; set; } + } +} diff --git a/src/App/Models/Api/Response/PreloginResponse.cs b/src/Core/Models/Response/PreloginResponse.cs similarity index 69% rename from src/App/Models/Api/Response/PreloginResponse.cs rename to src/Core/Models/Response/PreloginResponse.cs index c471d12a7..33cf5c385 100644 --- a/src/App/Models/Api/Response/PreloginResponse.cs +++ b/src/Core/Models/Response/PreloginResponse.cs @@ -1,6 +1,6 @@ -using Bit.App.Enums; +using Bit.Core.Enums; -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { public class PreloginResponse { diff --git a/src/App/Models/Api/Response/ProfileOrganizationResponse.cs b/src/Core/Models/Response/ProfileOrganizationResponse.cs similarity index 78% rename from src/App/Models/Api/Response/ProfileOrganizationResponse.cs rename to src/Core/Models/Response/ProfileOrganizationResponse.cs index bbaddd5c0..c1b6b8826 100644 --- a/src/App/Models/Api/Response/ProfileOrganizationResponse.cs +++ b/src/Core/Models/Response/ProfileOrganizationResponse.cs @@ -1,8 +1,8 @@ -using Bit.App.Enums; +using Bit.Core.Enums; -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { - public class ProfileOrganizationResponseModel + public class ProfileOrganizationResponse { public string Id { get; set; } public string Name { get; set; } @@ -11,7 +11,9 @@ namespace Bit.App.Models.Api public bool UseEvents { get; set; } public bool UseTotp { get; set; } public bool Use2fa { get; set; } + public bool UseApi { get; set; } public bool UsersGetPremium { get; set; } + public bool SelfHost { get; set; } public int Seats { get; set; } public int MaxCollections { get; set; } public short? MaxStorageGb { get; set; } diff --git a/src/App/Models/Api/Response/ProfileResponse.cs b/src/Core/Models/Response/ProfileResponse.cs similarity index 83% rename from src/App/Models/Api/Response/ProfileResponse.cs rename to src/Core/Models/Response/ProfileResponse.cs index 257e9fad9..07bd1ea70 100644 --- a/src/App/Models/Api/Response/ProfileResponse.cs +++ b/src/Core/Models/Response/ProfileResponse.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Bit.App.Models.Api +namespace Bit.Core.Models.Response { public class ProfileResponse { @@ -15,6 +15,6 @@ namespace Bit.App.Models.Api public string Key { get; set; } public string PrivateKey { get; set; } public string SecurityStamp { get; set; } - public IEnumerable Organizations { get; set; } + public List Organizations { get; set; } } } diff --git a/src/Core/Models/Response/SyncResponse.cs b/src/Core/Models/Response/SyncResponse.cs new file mode 100644 index 000000000..76e4fb46c --- /dev/null +++ b/src/Core/Models/Response/SyncResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class SyncResponse + { + public ProfileResponse Profile { get; set; } + public List Folders { get; set; } = new List(); + public List Collections { get; set; } = new List(); + public List Ciphers { get; set; } = new List(); + public DomainsResponse Domains { get; set; } + } +} diff --git a/src/Core/Models/View/AttachmentView.cs b/src/Core/Models/View/AttachmentView.cs new file mode 100644 index 000000000..a0782d4fb --- /dev/null +++ b/src/Core/Models/View/AttachmentView.cs @@ -0,0 +1,36 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class AttachmentView : View + { + public AttachmentView() { } + + public AttachmentView(Attachment a) + { + Id = a.Id; + Url = a.Url; + Size = a.Size; + SizeName = a.SizeName; + } + + public string Id { get; set; } + public string Url { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + public string FileName { get; set; } + public SymmetricCryptoKey Key { get; set; } + + public long FileSize + { + get + { + if(!string.IsNullOrWhiteSpace(Size) && long.TryParse(Size, out var s)) + { + return s; + } + return 0; + } + } + } +} diff --git a/src/Core/Models/View/CardView.cs b/src/Core/Models/View/CardView.cs new file mode 100644 index 000000000..f62e5b9f2 --- /dev/null +++ b/src/Core/Models/View/CardView.cs @@ -0,0 +1,89 @@ +using Bit.Core.Models.Domain; +using System.Text.RegularExpressions; + +namespace Bit.Core.Models.View +{ + public class CardView : View + { + private string _brand; + private string _number; + private string _subTitle; + + public CardView() { } + + public CardView(Card c) { } + + public string CardholderName { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + public string MaskedCode => Code != null ? new string('•', Code.Length) : null; + + public string Brand + { + get => _brand; + set + { + _brand = value; + _subTitle = null; + } + } + + public string Number + { + get => _number; + set + { + _number = value; + _subTitle = null; + } + } + + public string SubTitle + { + get + { + if(_subTitle == null) + { + _subTitle = Brand; + if(Number != null && Number.Length >= 4) + { + if(string.IsNullOrWhiteSpace(_subTitle)) + { + _subTitle += ", "; + } + else + { + _subTitle = string.Empty; + } + // Show last 5 on amex, last 4 for all others + var count = Number.Length >= 5 && Regex.Match(Number, "^3[47]").Success ? 5 : 4; + _subTitle += ("*" + Number.Substring(Number.Length - count)); + } + } + return _subTitle; + } + } + + public string Expiration + { + get + { + var expMonthNull = string.IsNullOrWhiteSpace(ExpMonth); + var expYearNull = string.IsNullOrWhiteSpace(ExpYear); + if(expMonthNull && expYearNull) + { + return null; + } + var expMo = !expMonthNull ? ExpMonth.PadLeft(2, '0') : "__"; + var expYr = !expYearNull ? FormatYear(ExpYear) : "____"; + return string.Format("{0} / {1}", expMo, expYr); + } + } + + private string FormatYear(string year) + { + return year.Length == 2 ? string.Concat("20", year) : year; + } + } +} diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs new file mode 100644 index 000000000..1ca6c3f90 --- /dev/null +++ b/src/Core/Models/View/CipherView.cs @@ -0,0 +1,100 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.View +{ + public class CipherView : View + { + public CipherView() { } + + public CipherView(Cipher c) + { + Id = c.Id; + OrganizationId = c.OrganizationId; + FolderId = c.FolderId; + Favorite = c.Favorite; + OrganizationUseTotp = c.OrganizationUseTotp; + Edit = c.Edit; + Type = c.Type; + LocalData = c.LocalData; + CollectionIds = c.CollectionIds; + RevisionDate = c.RevisionDate; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string FolderId { get; set; } + public string Name { get; set; } + public string Notes { get; set; } + public CipherType Type { get; set; } + public bool Favorite { get; set; } + public bool OrganizationUseTotp { get; set; } + public bool Edit { get; set; } + public Dictionary LocalData { get; set; } + public LoginView Login { get; set; } + public IdentityView Identity { get; set; } + public CardView Card { get; set; } + public SecureNoteView SecureNote { get; set; } + public List Attachments { get; set; } + public List Fields { get; set; } + public List PasswordHistory { get; set; } + public HashSet CollectionIds { get; set; } + public DateTime RevisionDate { get; set; } + + + public string SubTitle + { + get + { + switch(Type) + { + case CipherType.Login: + return Login.SubTitle; + case CipherType.SecureNote: + return SecureNote.SubTitle; + case CipherType.Card: + return Card.SubTitle; + case CipherType.Identity: + return Identity.SubTitle; + default: + break; + } + return null; + } + } + + public bool Shared => OrganizationId != null; + public bool HasPasswordHistory => PasswordHistory?.Any() ?? false; + public bool HasAttachments => Attachments?.Any() ?? false; + public bool HasOldAttachments + { + get + { + if(HasAttachments) + { + return Attachments.Any(a => a.Key == null); + } + return false; + } + } + public bool HasFields => Fields?.Any() ?? false; + public DateTime? PasswordRevisionDisplayDate + { + get + { + if(Type != CipherType.Login || Login == null) + { + return null; + } + else if(string.IsNullOrWhiteSpace(Login.Password)) + { + return null; + } + return Login.PasswordRevisionDate; + } + } + } +} diff --git a/src/Core/Models/View/CollectionView.cs b/src/Core/Models/View/CollectionView.cs new file mode 100644 index 000000000..23251a642 --- /dev/null +++ b/src/Core/Models/View/CollectionView.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class CollectionView : View, ITreeNodeObject + { + public CollectionView() { } + + public CollectionView(Collection c) + { + Id = c.Id; + OrganizationId = c.OrganizationId; + ReadOnly = c.ReadOnly; + ExternalId = c.ExternalId; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public string Name { get; set; } + public string ExternalId { get; set; } + public bool ReadOnly { get; set; } + } +} diff --git a/src/Core/Models/View/FieldView.cs b/src/Core/Models/View/FieldView.cs new file mode 100644 index 000000000..5fa93a68f --- /dev/null +++ b/src/Core/Models/View/FieldView.cs @@ -0,0 +1,20 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class FieldView : View + { + public FieldView() { } + + public FieldView(Field f) + { + Type = f.Type; + } + + public string Name { get; set; } + public string Value { get; set; } + public FieldType Type { get; set; } + public string MaskedValue => Value != null ? "••••••••" : null; + } +} diff --git a/src/Core/Models/View/FolderView.cs b/src/Core/Models/View/FolderView.cs new file mode 100644 index 000000000..e09eea1d3 --- /dev/null +++ b/src/Core/Models/View/FolderView.cs @@ -0,0 +1,20 @@ +using Bit.Core.Models.Domain; +using System; + +namespace Bit.Core.Models.View +{ + public class FolderView : View, ITreeNodeObject + { + public FolderView() { } + + public FolderView(Folder f) + { + Id = f.Id; + RevisionDate = f.RevisionDate; + } + + public string Id { get; set; } + public string Name { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Core/Models/View/IdentityView.cs b/src/Core/Models/View/IdentityView.cs new file mode 100644 index 000000000..3d346537d --- /dev/null +++ b/src/Core/Models/View/IdentityView.cs @@ -0,0 +1,145 @@ +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class IdentityView : View + { + private string _firstName; + private string _lastName; + private string _subTitle; + + public IdentityView() { } + + public IdentityView(Identity i) { } + + public string Title { get; set; } + public string FirstName + { + get => _firstName; + set + { + _firstName = value; + _subTitle = null; + } + } + public string MiddleName { get; set; } + public string LastName + { + get => _lastName; + set + { + _lastName = value; + _subTitle = null; + } + } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Address3 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string Company { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string SSN { get; set; } + public string Username { get; set; } + public string PassportNumber { get; set; } + public string LicenseNumber { get; set; } + + public string SubTitle + { + get + { + if(_subTitle == null && (FirstName != null || LastName != null)) + { + _subTitle = string.Empty; + if(FirstName != null) + { + _subTitle = FirstName; + } + if(LastName != null) + { + if(_subTitle != string.Empty) + { + _subTitle += " "; + } + _subTitle += LastName; + } + } + return _subTitle; + } + } + + public string FullName + { + get + { + if(!string.IsNullOrWhiteSpace(Title) || !string.IsNullOrWhiteSpace(FirstName) || + !string.IsNullOrWhiteSpace(MiddleName) || !string.IsNullOrWhiteSpace(LastName)) + { + var name = string.Empty; + if(!string.IsNullOrWhiteSpace(Title)) + { + name = string.Concat(name, Title, " "); + } + if(!string.IsNullOrWhiteSpace(FirstName)) + { + name = string.Concat(name, FirstName, " "); + } + if(!string.IsNullOrWhiteSpace(MiddleName)) + { + name = string.Concat(name, MiddleName, " "); + } + if(!string.IsNullOrWhiteSpace(LastName)) + { + name = string.Concat(name, LastName); + } + return name.Trim(); + } + return null; + } + } + + public string FullAddress + { + get + { + var address = Address1; + if(!string.IsNullOrWhiteSpace(Address2)) + { + if(!string.IsNullOrWhiteSpace(address)) + { + address += ", "; + } + address += Address2; + } + if(!string.IsNullOrWhiteSpace(Address3)) + { + if(!string.IsNullOrWhiteSpace(address)) + { + address += ", "; + } + address += Address3; + } + return address; + } + } + + public string FullAddressPart2 + { + get + { + if(string.IsNullOrWhiteSpace(City) && string.IsNullOrWhiteSpace(State) && + string.IsNullOrWhiteSpace(PostalCode)) + { + return null; + } + var city = string.IsNullOrWhiteSpace(City) ? "-" : City; + var state = string.IsNullOrWhiteSpace(State) ? "-" : State; + var postalCode = string.IsNullOrWhiteSpace(PostalCode) ? "-" : PostalCode; + return string.Format("{0}, {1}, {2}", city, state, postalCode); + } + } + } +} diff --git a/src/Core/Models/View/LoginUriView.cs b/src/Core/Models/View/LoginUriView.cs new file mode 100644 index 000000000..8dc3e27e2 --- /dev/null +++ b/src/Core/Models/View/LoginUriView.cs @@ -0,0 +1,109 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Bit.Core.Models.View +{ + public class LoginUriView : View + { + private HashSet _canLaunchWhitelist = new HashSet + { + "https://", + "http://", + "ssh://", + "ftp://", + "sftp://", + "irc://", + "vnc://", + "chrome://", + "iosapp://", + "androidapp://", + }; + + private string _uri; + private string _domain; + private string _hostname; + private bool? _canLaunch; + + public LoginUriView() { } + + public LoginUriView(LoginUri u) + { + Match = u.Match; + } + + public UriMatchType? Match { get; set; } + public string Uri + { + get => _uri; + set + { + _uri = value; + _domain = null; + _canLaunch = null; + } + } + + public string Domain + { + get + { + if(_domain == null && Uri != null) + { + _domain = CoreHelpers.GetDomain(Uri); + if(_domain == string.Empty) + { + _domain = null; + } + } + return _domain; + } + } + + public string Hostname + { + get + { + if(_hostname == null && Uri != null) + { + _hostname = CoreHelpers.GetHostname(Uri); + if(_hostname == string.Empty) + { + _hostname = null; + } + } + return _hostname; + } + } + + public string HostnameOrUri => Hostname ?? Uri; + + public bool IsWebsite => Uri != null && (Uri.StartsWith("http://") || Uri.StartsWith("https://") || + (Uri.Contains("://") && Regex.IsMatch(Uri, CoreHelpers.TldEndingRegex))); + + public bool CanLaunch + { + get + { + if(_canLaunch != null) + { + return _canLaunch.Value; + } + if(Uri != null && Match != UriMatchType.RegularExpression) + { + var uri = LaunchUri; + _canLaunch = _canLaunchWhitelist.Any(prefix => uri.StartsWith(prefix)); + return _canLaunch.Value; + } + _canLaunch = false; + return _canLaunch.Value; + } + } + + public string LaunchUri => !Uri.Contains("://") && Regex.IsMatch(Uri, CoreHelpers.TldEndingRegex) ? + string.Concat("http://", Uri) : Uri; + } +} diff --git a/src/Core/Models/View/LoginView.cs b/src/Core/Models/View/LoginView.cs new file mode 100644 index 000000000..e0d0ba3b5 --- /dev/null +++ b/src/Core/Models/View/LoginView.cs @@ -0,0 +1,29 @@ +using Bit.Core.Models.Domain; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.View +{ + public class LoginView : View + { + public LoginView() { } + + public LoginView(Login l) + { + PasswordRevisionDate = l.PasswordRevisionDate; + } + + public string Username { get; set; } + public string Password { get; set; } + public DateTime? PasswordRevisionDate { get; set; } + public string Totp { get; set; } + public List Uris { get; set; } + public string Uri => HasUris ? Uris[0].Uri : null; + public string MaskedPassword => Password != null ? "••••••••" : null; + public string SubTitle => Username; + public bool CanLaunch => HasUris && Uris.Any(u => u.CanLaunch); + public string LaunchUri => HasUris ? Uris.FirstOrDefault(u => u.CanLaunch)?.LaunchUri : null; + public bool HasUris => (Uris?.Count ?? 0) > 0; + } +} diff --git a/src/Core/Models/View/PasswordHistoryView.cs b/src/Core/Models/View/PasswordHistoryView.cs new file mode 100644 index 000000000..03d646730 --- /dev/null +++ b/src/Core/Models/View/PasswordHistoryView.cs @@ -0,0 +1,18 @@ +using Bit.Core.Models.Domain; +using System; + +namespace Bit.Core.Models.View +{ + public class PasswordHistoryView : View + { + public PasswordHistoryView() { } + + public PasswordHistoryView(PasswordHistory ph) + { + LastUsedDate = ph.LastUsedDate; + } + + public string Password { get; set; } + public DateTime LastUsedDate { get; set; } + } +} diff --git a/src/Core/Models/View/SecureNoteView.cs b/src/Core/Models/View/SecureNoteView.cs new file mode 100644 index 000000000..694f8ba6e --- /dev/null +++ b/src/Core/Models/View/SecureNoteView.cs @@ -0,0 +1,18 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Models.View +{ + public class SecureNoteView : View + { + public SecureNoteView() { } + + public SecureNoteView(SecureNote n) + { + Type = n.Type; + } + + public SecureNoteType Type { get; set; } + public string SubTitle => null; + } +} diff --git a/src/Core/Models/View/View.cs b/src/Core/Models/View/View.cs new file mode 100644 index 000000000..eaaed0f91 --- /dev/null +++ b/src/Core/Models/View/View.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.View +{ + public abstract class View + { + // TODO + } +} diff --git a/src/App/Resources/public_suffix_list.dat b/src/Core/Resources/public_suffix_list.dat similarity index 100% rename from src/App/Resources/public_suffix_list.dat rename to src/Core/Resources/public_suffix_list.dat diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs new file mode 100644 index 000000000..600d1f11c --- /dev/null +++ b/src/Core/Services/ApiService.cs @@ -0,0 +1,456 @@ +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class ApiService : IApiService + { + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + private readonly HttpClient _httpClient = new HttpClient(); + private readonly ITokenService _tokenService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly Func _logoutCallbackAsync; + private string _deviceType; + private bool _usingBaseUrl = false; + + public ApiService( + ITokenService tokenService, + IPlatformUtilsService platformUtilsService, + Func logoutCallbackAsync) + { + _tokenService = tokenService; + _platformUtilsService = platformUtilsService; + _logoutCallbackAsync = logoutCallbackAsync; + var device = _platformUtilsService.GetDevice(); + _deviceType = device.ToString(); + } + + public bool UrlsSet { get; private set; } + public string ApiBaseUrl { get; set; } + public string IdentityBaseUrl { get; set; } + + public void SetUrls(EnvironmentUrls urls) + { + UrlsSet = true; + if(!string.IsNullOrWhiteSpace(urls.Base)) + { + _usingBaseUrl = true; + ApiBaseUrl = urls.Base + "/api"; + IdentityBaseUrl = urls.Base + "/identity"; + return; + } + if(!string.IsNullOrWhiteSpace(urls.Api) && !string.IsNullOrWhiteSpace(urls.Identity)) + { + ApiBaseUrl = urls.Api; + IdentityBaseUrl = urls.Identity; + return; + } + // Local Dev + //ApiBaseUrl = "http://localhost:4000"; + //IdentityBaseUrl = "http://localhost:33656"; + // Production + ApiBaseUrl = "https://api.bitwarden.com"; + IdentityBaseUrl = "https://identity.bitwarden.com"; + } + + #region Auth APIs + + public async Task> PostIdentityTokenAsync( + TokenRequest request) + { + var requestMessage = new HttpRequestMessage + { + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(request.ToIdentityToken(_platformUtilsService.IdentityClientId)) + }; + requestMessage.Headers.Add("Accept", "application/json"); + requestMessage.Headers.Add("Device-Type", _deviceType); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch + { + throw new ApiException(HandleWebError()); + } + JObject responseJObject = null; + if(IsJsonResponse(response)) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + + if(responseJObject != null) + { + if(response.IsSuccessStatusCode) + { + return new Tuple( + responseJObject.ToObject(), null); + } + else if(response.StatusCode == HttpStatusCode.BadRequest && + responseJObject.ContainsKey("TwoFactorProviders2") && + responseJObject["TwoFactorProviders2"] != null && + responseJObject["TwoFactorProviders2"].HasValues) + { + return new Tuple( + null, responseJObject.ToObject()); + } + } + throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); + } + + public async Task RefreshIdentityTokenAsync() + { + try + { + await DoRefreshTokenAsync(); + } + catch + { + throw new ApiException(); + } + } + + #endregion + + #region Account APIs + + public Task GetProfileAsync() + { + return SendAsync(HttpMethod.Get, "/accounts/profile", null, true, true); + } + + public Task PostPreloginAsync(PreloginRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/prelogin", + request, false, true); + } + + public Task GetAccountRevisionDateAsync() + { + return SendAsync(HttpMethod.Get, "/accounts/revision-date", null, true, true); + } + + public Task PostPasswordHintAsync(PasswordHintRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/password-hint", + request, false, false); + } + + public Task PostRegisterAsync(RegisterRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/register", request, false, false); + } + + public Task PostAccountKeysAsync(KeysRequest request) + { + return SendAsync(HttpMethod.Post, "/accounts/keys", request, true, false); + } + + #endregion + + #region Folder APIs + + public Task GetFolderAsync(string id) + { + return SendAsync(HttpMethod.Get, string.Concat("/folders/", id), + null, true, true); + } + + public Task PostFolderAsync(FolderRequest request) + { + return SendAsync(HttpMethod.Post, "/folders", request, true, true); + } + + public async Task PutFolderAsync(string id, FolderRequest request) + { + return await SendAsync(HttpMethod.Put, string.Concat("/folders/", id), + request, true, true); + } + + public Task DeleteFolderAsync(string id) + { + return SendAsync(HttpMethod.Delete, string.Concat("/folders/", id), null, true, false); + } + + #endregion + + #region Cipher APIs + + public Task GetCipherAsync(string id) + { + return SendAsync(HttpMethod.Get, string.Concat("/ciphers/", id), + null, true, true); + } + + public Task PostCipherAsync(CipherRequest request) + { + return SendAsync(HttpMethod.Post, "/ciphers", request, true, true); + } + + public Task PostCipherCreateAsync(CipherCreateRequest request) + { + return SendAsync(HttpMethod.Post, "/ciphers/create", + request, true, true); + } + + public Task PutCipherAsync(string id, CipherRequest request) + { + return SendAsync(HttpMethod.Put, string.Concat("/ciphers/", id), + request, true, true); + } + + public Task PutShareCipherAsync(string id, CipherShareRequest request) + { + return SendAsync(HttpMethod.Put, + string.Concat("/ciphers/", id, "/share"), request, true, true); + } + + public Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request) + { + return SendAsync(HttpMethod.Put, + string.Concat("/ciphers/", id, "/collections"), request, true, false); + } + + public Task DeleteCipherAsync(string id) + { + return SendAsync(HttpMethod.Delete, string.Concat("/ciphers/", id), null, true, false); + } + + #endregion + + #region Attachments APIs + + public Task PostCipherAttachmentAsync(string id, MultipartFormDataContent data) + { + return SendAsync(HttpMethod.Post, + string.Concat("/ciphers/", id, "/attachment"), data, true, true); + } + + public Task DeleteCipherAttachmentAsync(string id, string attachmentId) + { + return SendAsync(HttpMethod.Delete, + string.Concat("/ciphers/", id, "/attachment/", attachmentId), null, true, false); + } + + public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, + string organizationId) + { + return SendAsync(HttpMethod.Post, + string.Concat("/ciphers/", id, "/attachment/", attachmentId, "/share?organizationId=", organizationId), + data, true, false); + } + + #endregion + + #region Sync APIs + + public Task GetSyncAsync() + { + return SendAsync(HttpMethod.Get, "/sync", null, true, true); + } + + #endregion + + #region Two Factor APIs + + public Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request) + { + return SendAsync( + HttpMethod.Post, "/two-factor/send-email-login", request, false, false); + } + + #endregion + + #region Device APIs + + public Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request) + { + return SendAsync( + HttpMethod.Post, $"identifier/{identifier}/token", request, true, false); + } + + #endregion + + #region HIBP APIs + + public Task> GetHibpBreachAsync(string username) + { + return SendAsync>(HttpMethod.Get, + string.Concat("/hibp/breach?username=", username), null, true, true); + } + + #endregion + + #region Helpers + + public async Task GetActiveBearerTokenAsync() + { + var accessToken = await _tokenService.GetTokenAsync(); + if(_tokenService.TokenNeedsRefresh()) + { + var tokenResponse = await DoRefreshTokenAsync(); + accessToken = tokenResponse.AccessToken; + } + return accessToken; + } + + public async Task SendAsync(HttpMethod method, string path, TRequest body, + bool authed, bool hasResponse) + { + using(var requestMessage = new HttpRequestMessage()) + { + requestMessage.Method = method; + requestMessage.RequestUri = new Uri(string.Concat(ApiBaseUrl, path)); + if(body != null) + { + var bodyType = body.GetType(); + if(bodyType == typeof(string)) + { + requestMessage.Content = new StringContent((object)bodyType as string, Encoding.UTF8, + "application/x-www-form-urlencoded; charset=utf-8"); + } + else if(bodyType == typeof(MultipartFormDataContent)) + { + requestMessage.Content = body as MultipartFormDataContent; + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body, _jsonSettings), + Encoding.UTF8, "application/json"); + } + } + + requestMessage.Headers.Add("Device-Type", _deviceType); + if(authed) + { + var authHeader = await GetActiveBearerTokenAsync(); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + } + if(hasResponse) + { + requestMessage.Headers.Add("Accept", "application/json"); + } + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch + { + throw new ApiException(HandleWebError()); + } + if(hasResponse && response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); + } + else if(!response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false); + throw new ApiException(error); + } + return (TResponse)(object)null; + } + } + + public async Task DoRefreshTokenAsync() + { + var refreshToken = await _tokenService.GetRefreshTokenAsync(); + if(string.IsNullOrWhiteSpace(refreshToken)) + { + throw new ApiException(); + } + + var decodedToken = _tokenService.DecodeToken(); + var requestMessage = new HttpRequestMessage + { + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "refresh_token", + ["client_id"] = decodedToken.GetValue("client_id")?.Value(), + ["refresh_token"] = refreshToken + }) + }; + requestMessage.Headers.Add("Accept", "application/json"); + requestMessage.Headers.Add("Device-Type", _deviceType); + + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(requestMessage); + } + catch + { + throw new ApiException(HandleWebError()); + } + if(response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseJsonString); + await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); + return tokenResponse; + } + else + { + var error = await HandleErrorAsync(response, true); + throw new ApiException(error); + } + } + + private ErrorResponse HandleWebError() + { + return new ErrorResponse + { + StatusCode = HttpStatusCode.BadGateway, + Message = "There is a problem connecting to the server." + }; + } + + private async Task HandleErrorAsync(HttpResponseMessage response, bool tokenError) + { + if((tokenError && response.StatusCode == HttpStatusCode.BadRequest) || + response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) + { + await _logoutCallbackAsync(true); + return null; + } + JObject responseJObject = null; + if(IsJsonResponse(response)) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + return new ErrorResponse(responseJObject, response.StatusCode, tokenError); + } + + private bool IsJsonResponse(HttpResponseMessage response) + { + return (response.Content?.Headers?.ContentType?.MediaType ?? string.Empty) == "application/json"; + } + + #endregion + } +} diff --git a/src/Core/Services/AppIdService.cs b/src/Core/Services/AppIdService.cs new file mode 100644 index 000000000..a0207bf5f --- /dev/null +++ b/src/Core/Services/AppIdService.cs @@ -0,0 +1,38 @@ +using Bit.Core.Abstractions; +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class AppIdService : IAppIdService + { + private readonly IStorageService _storageService; + + public AppIdService(IStorageService storageService) + { + _storageService = storageService; + } + + public Task GetAppIdAsync() + { + return MakeAndGetAppIdAsync("appId"); + } + + public Task GetAnonymousAppIdAsync() + { + return MakeAndGetAppIdAsync("anonymousAppId"); + } + + private async Task MakeAndGetAppIdAsync(string key) + { + var existingId = await _storageService.GetAsync(key); + if(existingId != null) + { + return existingId; + } + var guid = Guid.NewGuid().ToString(); + await _storageService.SaveAsync(key, guid); + return guid; + } + } +} diff --git a/src/Core/Services/AuditService.cs b/src/Core/Services/AuditService.cs new file mode 100644 index 000000000..7724d3c0e --- /dev/null +++ b/src/Core/Services/AuditService.cs @@ -0,0 +1,62 @@ +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Response; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class AuditService : IAuditService + { + private const string PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/"; + + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IApiService _apiService; + + private HttpClient _httpClient = new HttpClient(); + + public AuditService( + ICryptoFunctionService cryptoFunctionService, + IApiService apiService) + { + _cryptoFunctionService = cryptoFunctionService; + _apiService = apiService; + } + + public async Task PasswordLeakedAsync(string password) + { + var hashBytes = await _cryptoFunctionService.HashAsync(password, Enums.CryptoHashAlgorithm.Sha1); + var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToUpperInvariant(); + var hashStart = hash.Substring(0, 5); + var hashEnding = hash.Substring(5); + var response = await _httpClient.GetAsync(string.Concat(PwnedPasswordsApi, hashStart)); + var leakedHashes = await response.Content.ReadAsStringAsync(); + var match = leakedHashes.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) + .FirstOrDefault(v => v.Split(':')[0] == hashEnding); + if(match != null && int.TryParse(match.Split(':')[1], out var matchCount)) + { + return matchCount; + } + return 0; + } + + public async Task> BreachedAccountsAsync(string username) + { + try + { + return await _apiService.GetHibpBreachAsync(username); + } + catch(ApiException e) + { + if(e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return new List(); + } + throw e; + } + } + } +} diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs new file mode 100644 index 000000000..73ce0653c --- /dev/null +++ b/src/Core/Services/AuthService.cs @@ -0,0 +1,327 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class AuthService : IAuthService + { + private readonly ICryptoService _cryptoService; + private readonly IApiService _apiService; + private readonly IUserService _userService; + private readonly ITokenService _tokenService; + private readonly IAppIdService _appIdService; + private readonly II18nService _i18nService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IMessagingService _messagingService; + private readonly bool _setCryptoKeys; + + private SymmetricCryptoKey _key; + private KdfType? _kdf; + private int? _kdfIterations; + + public AuthService( + ICryptoService cryptoService, + IApiService apiService, + IUserService userService, + ITokenService tokenService, + IAppIdService appIdService, + II18nService i18nService, + IPlatformUtilsService platformUtilsService, + IMessagingService messagingService, + bool setCryptoKeys = true) + { + _cryptoService = cryptoService; + _apiService = apiService; + _userService = userService; + _tokenService = tokenService; + _appIdService = appIdService; + _i18nService = i18nService; + _platformUtilsService = platformUtilsService; + _messagingService = messagingService; + _setCryptoKeys = setCryptoKeys; + + TwoFactorProviders = new Dictionary(); + TwoFactorProviders.Add(TwoFactorProviderType.Authenticator, new TwoFactorProvider + { + Type = TwoFactorProviderType.Authenticator, + Priority = 1, + Sort = 1 + }); + TwoFactorProviders.Add(TwoFactorProviderType.YubiKey, new TwoFactorProvider + { + Type = TwoFactorProviderType.YubiKey, + Priority = 3, + Sort = 2, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.Duo, new TwoFactorProvider + { + Type = TwoFactorProviderType.Duo, + Name = "Duo", + Priority = 2, + Sort = 3, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider + { + Type = TwoFactorProviderType.OrganizationDuo, + Name = "Duo (Organization)", + Priority = 10, + Sort = 4 + }); + TwoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider + { + Type = TwoFactorProviderType.U2f, + Priority = 4, + Sort = 5, + Premium = true + }); + TwoFactorProviders.Add(TwoFactorProviderType.Email, new TwoFactorProvider + { + Type = TwoFactorProviderType.Email, + Priority = 0, + Sort = 6, + }); + } + + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public Dictionary TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProvidersData { get; set; } + public TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } + + public void Init() + { + TwoFactorProviders[TwoFactorProviderType.Email].Name = _i18nService.T("Email"); + TwoFactorProviders[TwoFactorProviderType.Email].Description = _i18nService.T("EmailDesc"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Name = _i18nService.T("AuthenticatorAppTitle"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Description = + _i18nService.T("AuthenticatorAppDesc"); + TwoFactorProviders[TwoFactorProviderType.Duo].Description = _i18nService.T("DuoDesc"); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Name = + string.Format("Duo ({0})", _i18nService.T("Organization")); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description = + _i18nService.T("DuoOrganizationDesc"); + TwoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle"); + TwoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc"); + } + + public async Task LogInAsync(string email, string masterPassword) + { + SelectedTwoFactorProviderType = null; + var key = await MakePreloginKeyAsync(masterPassword, email); + var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); + return await LogInHelperAsync(email, hashedPassword, key); + } + + public Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, + bool? remember = null) + { + return LogInHelperAsync(Email, MasterPasswordHash, _key, twoFactorProvider, twoFactorToken, remember); + } + + public async Task LogInCompleteAsync(string email, string masterPassword, + TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null) + { + SelectedTwoFactorProviderType = null; + var key = await MakePreloginKeyAsync(masterPassword, email); + var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); + return await LogInHelperAsync(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); + } + + public void LogOut(Action callback) + { + callback.Invoke(); + _messagingService.Send("loggedOut"); + } + + public List GetSupportedTwoFactorProviders() + { + var providers = new List(); + if(TwoFactorProvidersData == null) + { + return providers; + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.OrganizationDuo) && + _platformUtilsService.SupportsDuo()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Authenticator)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.YubiKey)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.YubiKey]); + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Duo) && _platformUtilsService.SupportsDuo()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.U2f) && _platformUtilsService.SupportsU2f()) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.U2f]); + } + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email)) + { + providers.Add(TwoFactorProviders[TwoFactorProviderType.Email]); + } + return providers; + } + + public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported) + { + if(TwoFactorProvidersData == null) + { + return null; + } + if(SelectedTwoFactorProviderType != null && + TwoFactorProvidersData.ContainsKey(SelectedTwoFactorProviderType.Value)) + { + return SelectedTwoFactorProviderType.Value; + } + TwoFactorProviderType? providerType = null; + var providerPriority = -1; + foreach(var providerKvp in TwoFactorProvidersData) + { + if(TwoFactorProviders.ContainsKey(providerKvp.Key)) + { + var provider = TwoFactorProviders[providerKvp.Key]; + if(provider.Priority > providerPriority) + { + if(providerKvp.Key == TwoFactorProviderType.U2f && !u2fSupported) + { + continue; + } + providerType = providerKvp.Key; + providerPriority = provider.Priority; + } + } + } + return providerType; + } + + // Helpers + + private async Task MakePreloginKeyAsync(string masterPassword, string email) + { + email = email.Trim().ToLower(); + _kdf = null; + _kdfIterations = null; + try + { + var preloginResponse = await _apiService.PostPreloginAsync(new PreloginRequest { Email = email }); + if(preloginResponse != null) + { + _kdf = preloginResponse.Kdf; + _kdfIterations = preloginResponse.KdfIterations; + } + } + catch(ApiException e) + { + if(e.Error == null || e.Error.StatusCode != System.Net.HttpStatusCode.NotFound) + { + throw e; + } + } + return await _cryptoService.MakeKeyAsync(masterPassword, email, _kdf, _kdfIterations); + } + + private async Task LogInHelperAsync(string email, string hashedPassword, SymmetricCryptoKey key, + TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null) + { + var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email); + var appId = await _appIdService.GetAppIdAsync(); + var deviceRequest = new DeviceRequest(appId, _platformUtilsService); + var request = new TokenRequest + { + Email = email, + MasterPasswordHash = hashedPassword, + Device = deviceRequest, + Remember = false + }; + if(twoFactorToken != null && twoFactorProvider != null) + { + request.Provider = twoFactorProvider; + request.Token = twoFactorToken; + request.Remember = remember.GetValueOrDefault(); + } + else if(storedTwoFactorToken != null) + { + request.Provider = TwoFactorProviderType.Remember; + request.Token = storedTwoFactorToken; + } + + var response = await _apiService.PostIdentityTokenAsync(request); + ClearState(); + var result = new AuthResult + { + TwoFactor = response.Item2 != null + }; + if(result.TwoFactor) + { + // Two factor required. + var twoFactorResponse = response.Item2; + Email = email; + MasterPasswordHash = hashedPassword; + _key = _setCryptoKeys ? key : null; + TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2; + result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2; + return result; + } + + var tokenResponse = response.Item1; + if(tokenResponse.TwoFactorToken != null) + { + await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email); + } + await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); + await _userService.SetInformationAsync(_tokenService.GetUserId(), _tokenService.GetEmail(), + _kdf.Value, _kdfIterations.Value); + if(_setCryptoKeys) + { + await _cryptoService.SetKeyAsync(key); + await _cryptoService.SetKeyHashAsync(hashedPassword); + await _cryptoService.SetEncKeyAsync(tokenResponse.Key); + + // User doesn't have a key pair yet (old account), let's generate one for them. + if(tokenResponse.PrivateKey == null) + { + try + { + var keyPair = await _cryptoService.MakeKeyPairAsync(); + await _apiService.PostAccountKeysAsync(new KeysRequest + { + PublicKey = keyPair.Item1, + EncryptedPrivateKey = keyPair.Item2.EncryptedString + }); + tokenResponse.PrivateKey = keyPair.Item2.EncryptedString; + } + catch { } + } + + await _cryptoService.SetEncPrivateKeyAsync(tokenResponse.PrivateKey); + } + + _messagingService.Send("loggedIn"); + return result; + } + + private void ClearState() + { + Email = null; + MasterPasswordHash = null; + TwoFactorProvidersData = null; + SelectedTwoFactorProviderType = null; + } + } +} diff --git a/src/Core/Services/BroadcasterService.cs b/src/Core/Services/BroadcasterService.cs new file mode 100644 index 000000000..4ac47ce91 --- /dev/null +++ b/src/Core/Services/BroadcasterService.cs @@ -0,0 +1,45 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using System; +using System.Collections.Generic; + +namespace Bit.App.Services +{ + public class BroadcasterService : IBroadcasterService + { + private readonly Dictionary> _subscribers = new Dictionary>(); + + public void Send(Message message, string id = null) + { + if(!string.IsNullOrWhiteSpace(id)) + { + if(_subscribers.ContainsKey(id)) + { + _subscribers[id].Invoke(message); + } + return; + } + foreach(var sub in _subscribers) + { + sub.Value.Invoke(message); + } + } + + public void Subscribe(string id, Action messageCallback) + { + if(_subscribers.ContainsKey(id)) + { + return; + } + _subscribers.Add(id, messageCallback); + } + + public void Unsubscribe(string id) + { + if(_subscribers.ContainsKey(id)) + { + _subscribers.Remove(id); + } + } + } +} diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs new file mode 100644 index 000000000..23bd3e451 --- /dev/null +++ b/src/Core/Services/CipherService.cs @@ -0,0 +1,1200 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class CipherService : ICipherService + { + private const string Keys_CiphersFormat = "ciphers_{0}"; + private const string Keys_LocalData = "ciphersLocalData"; + private const string Keys_NeverDomains = "neverDomains"; + + private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", + "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; + private List _decryptedCipherCache; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly ISettingsService _settingsService; + private readonly IApiService _apiService; + private readonly IStorageService _storageService; + private readonly II18nService _i18nService; + private readonly Func _searchService; + private Dictionary> _domainMatchBlacklist = new Dictionary> + { + ["google.com"] = new HashSet { "script.google.com" } + }; + private readonly HttpClient _httpClient = new HttpClient(); + private Task> _getAllDecryptedTask; + + public CipherService( + ICryptoService cryptoService, + IUserService userService, + ISettingsService settingsService, + IApiService apiService, + IStorageService storageService, + II18nService i18nService, + Func searchService) + { + _cryptoService = cryptoService; + _userService = userService; + _settingsService = settingsService; + _apiService = apiService; + _storageService = storageService; + _i18nService = i18nService; + _searchService = searchService; + } + + private List DecryptedCipherCache + { + get => _decryptedCipherCache; + set + { + if(value == null) + { + _decryptedCipherCache?.Clear(); + } + _decryptedCipherCache = value; + if(_searchService != null) + { + if(value == null) + { + _searchService().ClearIndex(); + } + else + { + _searchService().IndexCiphersAsync(); + } + } + } + } + + public void ClearCache() + { + DecryptedCipherCache = null; + } + + public async Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, + Cipher originalCipher = null) + { + // Adjust password history + if(model.Id != null) + { + if(originalCipher == null) + { + originalCipher = await GetAsync(model.Id); + } + if(originalCipher != null) + { + var existingCipher = await originalCipher.DecryptAsync(); + if(model.PasswordHistory == null) + { + model.PasswordHistory = new List(); + } + if(model.Type == CipherType.Login && existingCipher.Type == CipherType.Login) + { + if(!string.IsNullOrWhiteSpace(existingCipher.Login.Password) && + existingCipher.Login.Password != model.Login.Password) + { + var now = DateTime.UtcNow; + var ph = new PasswordHistoryView + { + Password = existingCipher.Login.Password, + LastUsedDate = now + }; + model.Login.PasswordRevisionDate = now; + model.PasswordHistory.Insert(0, ph); + } + else + { + model.Login.PasswordRevisionDate = existingCipher.Login.PasswordRevisionDate; + } + } + if(existingCipher.HasFields) + { + var existingHiddenFields = existingCipher.Fields.Where(f => + f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name) && + !string.IsNullOrWhiteSpace(f.Value)); + var hiddenFields = model.Fields?.Where(f => + f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name)) ?? + new List(); + foreach(var ef in existingHiddenFields) + { + var matchedField = hiddenFields.FirstOrDefault(f => f.Name == ef.Name); + if(matchedField == null || matchedField.Value != ef.Value) + { + var ph = new PasswordHistoryView + { + Password = string.Format("{0}: {1}", ef.Name, ef.Value), + LastUsedDate = DateTime.UtcNow + }; + model.PasswordHistory.Insert(0, ph); + } + } + } + } + if(!model.PasswordHistory?.Any() ?? false) + { + model.PasswordHistory = null; + } + else if(model.PasswordHistory != null && model.PasswordHistory.Count > 5) + { + model.PasswordHistory = model.PasswordHistory.Take(5).ToList(); + } + } + + var cipher = new Cipher + { + Id = model.Id, + FolderId = model.FolderId, + Favorite = model.Favorite, + OrganizationId = model.OrganizationId, + Type = model.Type, + CollectionIds = model.CollectionIds + }; + + if(key == null && cipher.OrganizationId != null) + { + key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); + if(key == null) + { + throw new Exception("Cannot encrypt cipher for organization. No key."); + } + } + + var tasks = new List + { + EncryptObjPropertyAsync(model, cipher, new HashSet + { + "Name", + "Notes" + }, key), + EncryptCipherDataAsync(cipher, model, key), + EncryptFieldsAsync(model.Fields, key, cipher), + EncryptPasswordHistoriesAsync(model.PasswordHistory, key, cipher), + EncryptAttachmentsAsync(model.Attachments, key, cipher) + }; + await Task.WhenAll(tasks); + return cipher; + } + + public async Task GetAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var localData = await _storageService.GetAsync>>( + Keys_LocalData); + var ciphers = await _storageService.GetAsync>( + string.Format(Keys_CiphersFormat, userId)); + if(!ciphers?.ContainsKey(id) ?? true) + { + return null; + } + return new Cipher(ciphers[id], false, + localData?.ContainsKey(id) ?? false ? localData[id] : null); + } + + public async Task> GetAllAsync() + { + var userId = await _userService.GetUserIdAsync(); + var localData = await _storageService.GetAsync>>( + Keys_LocalData); + var ciphers = await _storageService.GetAsync>( + string.Format(Keys_CiphersFormat, userId)); + var response = ciphers?.Select(c => new Cipher(c.Value, false, + localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null)); + return response?.ToList() ?? new List(); + } + + public Task> GetAllDecryptedAsync() + { + if(DecryptedCipherCache != null) + { + return Task.FromResult(DecryptedCipherCache); + } + if(_getAllDecryptedTask != null && !_getAllDecryptedTask.IsCompleted && !_getAllDecryptedTask.IsFaulted) + { + return _getAllDecryptedTask; + } + async Task> doTask() + { + try + { + var hashKey = await _cryptoService.HasKeyAsync(); + if(!hashKey) + { + throw new Exception("No key."); + } + var decCiphers = new List(); + async Task decryptAndAddCipherAsync(Cipher cipher) + { + var c = await cipher.DecryptAsync(); + decCiphers.Add(c); + } + var tasks = new List(); + var ciphers = await GetAllAsync(); + foreach(var cipher in ciphers) + { + tasks.Add(decryptAndAddCipherAsync(cipher)); + } + await Task.WhenAll(tasks); + decCiphers = decCiphers.OrderBy(c => c, new CipherLocaleComparer(_i18nService)).ToList(); + DecryptedCipherCache = decCiphers; + return DecryptedCipherCache; + } + finally + { + + } + } + _getAllDecryptedTask = doTask(); + return _getAllDecryptedTask; + } + + public async Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true) + { + var ciphers = await GetAllDecryptedAsync(); + return ciphers.Where(cipher => + { + if(folder && cipher.FolderId == groupingId) + { + return true; + } + if(!folder && cipher.CollectionIds != null && cipher.CollectionIds.Contains(groupingId)) + { + return true; + } + return false; + }).ToList(); + } + + public async Task> GetAllDecryptedForUrlAsync(string url) + { + var all = await GetAllDecryptedByUrlAsync(url); + return all.Item1; + } + + public async Task, List, List>> GetAllDecryptedByUrlAsync( + string url, List includeOtherTypes = null) + { + if(string.IsNullOrWhiteSpace(url) && includeOtherTypes == null) + { + return new Tuple, List, List>( + new List(), new List(), new List()); + } + + var domain = CoreHelpers.GetDomain(url); + var mobileApp = UrlIsMobileApp(url); + + var mobileAppInfo = InfoFromMobileAppUrl(url); + var mobileAppWebUriString = mobileAppInfo?.Item1; + var mobileAppSearchTerms = mobileAppInfo?.Item2; + + var matchingDomainsTask = GetMatchingDomainsAsync(url, domain, mobileApp, mobileAppWebUriString); + var ciphersTask = GetAllDecryptedAsync(); + await Task.WhenAll(new List + { + matchingDomainsTask, + ciphersTask + }); + + var matchingDomains = await matchingDomainsTask; + var matchingDomainsSet = matchingDomains.Item1; + var matchingFuzzyDomainsSet = matchingDomains.Item2; + + var matchingLogins = new List(); + var matchingFuzzyLogins = new List(); + var others = new List(); + var ciphers = await ciphersTask; + + var defaultMatch = await _storageService.GetAsync(Constants.DefaultUriMatch); + if(defaultMatch == null) + { + defaultMatch = UriMatchType.Domain; + } + + foreach(var cipher in ciphers) + { + if(cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false)) + { + others.Add(cipher); + continue; + } + + if(cipher.Type != CipherType.Login || cipher.Login?.Uris == null || !cipher.Login.Uris.Any()) + { + continue; + } + + foreach(var u in cipher.Login.Uris) + { + if(string.IsNullOrWhiteSpace(u.Uri)) + { + continue; + } + var match = false; + switch(u.Match) + { + case null: + case UriMatchType.Domain: + match = CheckDefaultUriMatch(cipher, u, matchingLogins, matchingFuzzyLogins, + matchingDomainsSet, matchingFuzzyDomainsSet, mobileApp, mobileAppSearchTerms); + if(match && u.Domain != null) + { + if(_domainMatchBlacklist.ContainsKey(u.Domain)) + { + var domainUrlHost = CoreHelpers.GetHost(url); + if(_domainMatchBlacklist[u.Domain].Contains(domainUrlHost)) + { + match = false; + } + } + } + break; + case UriMatchType.Host: + var urlHost = CoreHelpers.GetHost(url); + match = urlHost != null && urlHost == CoreHelpers.GetHost(u.Uri); + if(match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.Exact: + match = url == u.Uri; + if(match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.StartsWith: + match = url.StartsWith(u.Uri); + if(match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.RegularExpression: + var regex = new Regex(u.Uri, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); + match = regex.IsMatch(url); + if(match) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + } + break; + case UriMatchType.Never: + default: + break; + } + if(match) + { + break; + } + } + } + return new Tuple, List, List>( + matchingLogins, matchingFuzzyLogins, others); + } + + public async Task GetLastUsedForUrlAsync(string url) + { + var ciphers = await GetAllDecryptedForUrlAsync(url); + return ciphers.OrderBy(c => c, new CipherLastUsedComparer()).FirstOrDefault(); + } + + public async Task UpdateLastUsedDateAsync(string id) + { + var ciphersLocalData = await _storageService.GetAsync>>( + Keys_LocalData); + if(ciphersLocalData == null) + { + ciphersLocalData = new Dictionary>(); + } + if(!ciphersLocalData.ContainsKey(id)) + { + ciphersLocalData.Add(id, new Dictionary()); + } + if(ciphersLocalData[id].ContainsKey("lastUsedDate")) + { + ciphersLocalData[id]["lastUsedDate"] = DateTime.UtcNow; + } + else + { + ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow); + } + + await _storageService.SaveAsync(Keys_LocalData, ciphersLocalData); + // Update cache + if(DecryptedCipherCache == null) + { + return; + } + var cached = DecryptedCipherCache.FirstOrDefault(c => c.Id == id); + if(cached != null) + { + cached.LocalData = ciphersLocalData[id]; + } + } + + public async Task SaveNeverDomainAsync(string domain) + { + if(string.IsNullOrWhiteSpace(domain)) + { + return; + } + var domains = await _storageService.GetAsync>(Keys_NeverDomains); + if(domains == null) + { + domains = new HashSet(); + } + domains.Add(domain); + await _storageService.SaveAsync(Keys_NeverDomains, domains); + } + + public async Task SaveWithServerAsync(Cipher cipher) + { + CipherResponse response; + if(cipher.Id == null) + { + if(cipher.CollectionIds != null) + { + var request = new CipherCreateRequest(cipher); + response = await _apiService.PostCipherCreateAsync(request); + } + else + { + var request = new CipherRequest(cipher); + response = await _apiService.PostCipherAsync(request); + } + cipher.Id = response.Id; + } + else + { + var request = new CipherRequest(cipher); + response = await _apiService.PutCipherAsync(cipher.Id, request); + } + var userId = await _userService.GetUserIdAsync(); + var data = new CipherData(response, userId, cipher.CollectionIds); + await UpsertAsync(data); + } + + public async Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet collectionIds) + { + var attachmentTasks = new List(); + if(cipher.Attachments != null) + { + foreach(var attachment in cipher.Attachments) + { + if(attachment.Key == null) + { + attachmentTasks.Add(ShareAttachmentWithServerAsync(attachment, cipher.Id, organizationId)); + } + } + } + await Task.WhenAll(attachmentTasks); + cipher.OrganizationId = organizationId; + cipher.CollectionIds = collectionIds; + var encCipher = await EncryptAsync(cipher); + var request = new CipherShareRequest(encCipher); + var response = await _apiService.PutShareCipherAsync(cipher.Id, request); + var userId = await _userService.GetUserIdAsync(); + var data = new CipherData(response, userId, collectionIds); + await UpsertAsync(data); + } + + public async Task SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data) + { + var key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId); + var encFileName = await _cryptoService.EncryptAsync(filename, key); + var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); + var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1); + var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks); + var fd = new MultipartFormDataContent(boundary); + fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key"); + fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); + var response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd); + var userId = await _userService.GetUserIdAsync(); + var cData = new CipherData(response, userId, cipher.CollectionIds); + await UpsertAsync(cData); + return new Cipher(cData); + } + + public async Task SaveCollectionsWithServerAsync(Cipher cipher) + { + var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList()); + await _apiService.PutCipherCollectionsAsync(cipher.Id, request); + var userId = await _userService.GetUserIdAsync(); + var data = cipher.ToCipherData(userId); + await UpsertAsync(data); + } + + public async Task UpsertAsync(CipherData cipher) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CiphersFormat, userId); + var ciphers = await _storageService.GetAsync>(storageKey); + if(ciphers == null) + { + ciphers = new Dictionary(); + } + if(!ciphers.ContainsKey(cipher.Id)) + { + ciphers.Add(cipher.Id, null); + } + ciphers[cipher.Id] = cipher; + await _storageService.SaveAsync(storageKey, ciphers); + DecryptedCipherCache = null; + } + + public async Task UpsertAsync(List cipher) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CiphersFormat, userId); + var ciphers = await _storageService.GetAsync>(storageKey); + if(ciphers == null) + { + ciphers = new Dictionary(); + } + foreach(var c in cipher) + { + if(!ciphers.ContainsKey(c.Id)) + { + ciphers.Add(c.Id, null); + } + ciphers[c.Id] = c; + } + await _storageService.SaveAsync(storageKey, ciphers); + DecryptedCipherCache = null; + } + + public async Task ReplaceAsync(Dictionary ciphers) + { + var userId = await _userService.GetUserIdAsync(); + await _storageService.SaveAsync(string.Format(Keys_CiphersFormat, userId), ciphers); + DecryptedCipherCache = null; + } + + public async Task ClearAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_CiphersFormat, userId)); + ClearCache(); + } + + public async Task DeleteAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var cipherKey = string.Format(Keys_CiphersFormat, userId); + var ciphers = await _storageService.GetAsync>(cipherKey); + if(ciphers == null) + { + return; + } + if(!ciphers.ContainsKey(id)) + { + return; + } + ciphers.Remove(id); + await _storageService.SaveAsync(cipherKey, ciphers); + DecryptedCipherCache = null; + } + + public async Task DeleteAsync(List ids) + { + var userId = await _userService.GetUserIdAsync(); + var cipherKey = string.Format(Keys_CiphersFormat, userId); + var ciphers = await _storageService.GetAsync>(cipherKey); + if(ciphers == null) + { + return; + } + foreach(var id in ids) + { + if(!ciphers.ContainsKey(id)) + { + return; + } + ciphers.Remove(id); + } + await _storageService.SaveAsync(cipherKey, ciphers); + DecryptedCipherCache = null; + } + + public async Task DeleteWithServerAsync(string id) + { + await _apiService.DeleteCipherAsync(id); + await DeleteAsync(id); + } + + public async Task DeleteAttachmentAsync(string id, string attachmentId) + { + var userId = await _userService.GetUserIdAsync(); + var cipherKey = string.Format(Keys_CiphersFormat, userId); + var ciphers = await _storageService.GetAsync>(cipherKey); + if(ciphers == null || !ciphers.ContainsKey(id) || ciphers[id].Attachments == null) + { + return; + } + var attachment = ciphers[id].Attachments.FirstOrDefault(a => a.Id == attachmentId); + if(attachment != null) + { + ciphers[id].Attachments.Remove(attachment); + } + await _storageService.SaveAsync(cipherKey, ciphers); + DecryptedCipherCache = null; + } + + public async Task DeleteAttachmentWithServerAsync(string id, string attachmentId) + { + try + { + await _apiService.DeleteCipherAttachmentAsync(id, attachmentId); + await DeleteAttachmentAsync(id, attachmentId); + } + catch(ApiException e) + { + await DeleteAttachmentAsync(id, attachmentId); + throw e; + } + } + + public async Task DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId) + { + try + { + var response = await _httpClient.GetAsync(new Uri(attachment.Url)); + if(!response.IsSuccessStatusCode) + { + return null; + } + var data = await response.Content.ReadAsByteArrayAsync(); + if(data == null) + { + return null; + } + var key = attachment.Key ?? await _cryptoService.GetOrgKeyAsync(organizationId); + return await _cryptoService.DecryptFromBytesAsync(data, key); + } + catch { } + return null; + } + + // Helpers + + private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId, + string organizationId) + { + var attachmentResponse = await _httpClient.GetAsync(attachmentView.Url); + if(!attachmentResponse.IsSuccessStatusCode) + { + throw new Exception("Failed to download attachment: " + attachmentResponse.StatusCode); + } + + var bytes = await attachmentResponse.Content.ReadAsByteArrayAsync(); + var decBytes = await _cryptoService.DecryptFromBytesAsync(bytes, null); + var key = await _cryptoService.GetOrgKeyAsync(organizationId); + var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key); + var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); + var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1); + var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks); + var fd = new MultipartFormDataContent(boundary); + fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key"); + fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); + await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId); + } + + private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri, + List matchingLogins, List matchingFuzzyLogins, + HashSet matchingDomainsSet, HashSet matchingFuzzyDomainsSet, + bool mobileApp, string[] mobileAppSearchTerms) + { + var loginUriString = loginUri.Uri; + var loginUriDomain = loginUri.Domain; + + if(matchingDomainsSet.Contains(loginUriString)) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + return true; + } + else if(mobileApp && matchingFuzzyDomainsSet.Contains(loginUriString)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + else if(!mobileApp) + { + var info = InfoFromMobileAppUrl(loginUriString); + if(info?.Item1 != null && matchingDomainsSet.Contains(info.Item1)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + + if(!loginUri.Uri.Contains("://") && loginUriString.Contains(".")) + { + loginUriString = "http://" + loginUriString; + } + + if(loginUriDomain != null) + { + loginUriDomain = loginUriDomain.ToLowerInvariant(); + if(matchingDomainsSet.Contains(loginUriDomain)) + { + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); + return true; + } + else if(mobileApp && matchingFuzzyDomainsSet.Contains(loginUriDomain)) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + + if(mobileApp && (mobileAppSearchTerms?.Any() ?? false)) + { + var addedFromSearchTerm = false; + var loginName = cipher.Name?.ToLowerInvariant(); + foreach(var term in mobileAppSearchTerms) + { + addedFromSearchTerm = (loginUriDomain != null && loginUriDomain.Contains(term)) || + (loginName != null && loginName.Contains(term)); + if(!addedFromSearchTerm) + { + var domainTerm = loginUriDomain?.Split('.')[0]; + addedFromSearchTerm = + (domainTerm != null && domainTerm.Length > 2 && term.Contains(domainTerm)) || + (loginName != null && term.Contains(loginName)); + } + if(addedFromSearchTerm) + { + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; + } + } + } + + return false; + } + + private void AddMatchingLogin(CipherView cipher, List matchingLogins, + List matchingFuzzyLogins) + { + if(matchingFuzzyLogins.Contains(cipher)) + { + matchingFuzzyLogins.Remove(cipher); + } + matchingLogins.Add(cipher); + } + + private void AddMatchingFuzzyLogin(CipherView cipher, List matchingLogins, + List matchingFuzzyLogins) + { + if(!matchingFuzzyLogins.Contains(cipher) && !matchingLogins.Contains(cipher)) + { + matchingFuzzyLogins.Add(cipher); + } + } + + private async Task, HashSet>> GetMatchingDomainsAsync(string url, + string domain, bool mobileApp, string mobileAppWebUriString) + { + var matchingDomains = new HashSet(); + var matchingFuzzyDomains = new HashSet(); + var eqDomains = await _settingsService.GetEquivalentDomainsAsync(); + foreach(var eqDomain in eqDomains) + { + var eqDomainSet = new HashSet(eqDomain); + if(mobileApp) + { + if(eqDomainSet.Contains(url)) + { + eqDomain.Select(d => matchingDomains.Add(d)); + } + else if(mobileAppWebUriString != null && eqDomainSet.Contains(mobileAppWebUriString)) + { + eqDomain.Select(d => matchingFuzzyDomains.Add(d)); + } + } + else if(eqDomainSet.Contains(url)) + { + eqDomain.Select(d => matchingDomains.Add(d)); + } + } + if(!matchingDomains.Any()) + { + matchingDomains.Add(mobileApp ? url : domain); + } + if(mobileApp && mobileAppWebUriString != null && + !matchingFuzzyDomains.Any() && !matchingDomains.Contains(mobileAppWebUriString)) + { + matchingFuzzyDomains.Add(mobileAppWebUriString); + } + return new Tuple, HashSet>(matchingDomains, matchingFuzzyDomains); + } + + private Tuple InfoFromMobileAppUrl(string mobileAppUrlString) + { + if(UrlIsAndroidApp(mobileAppUrlString)) + { + return InfoFromAndroidAppUri(mobileAppUrlString); + } + else if(UrlIsiOSApp(mobileAppUrlString)) + { + return InfoFromiOSAppUrl(mobileAppUrlString); + } + return null; + } + + private Tuple InfoFromAndroidAppUri(string androidAppUrlString) + { + if(!UrlIsAndroidApp(androidAppUrlString)) + { + return null; + } + var androidUrlParts = androidAppUrlString.Replace(Constants.AndroidAppProtocol, string.Empty).Split('.'); + if(androidUrlParts.Length >= 2) + { + var webUri = string.Join(".", androidUrlParts[1], androidUrlParts[0]); + var searchTerms = androidUrlParts.Where(p => !_ignoredSearchTerms.Contains(p)) + .Select(p => p.ToLowerInvariant()).ToArray(); + return new Tuple(webUri, searchTerms); + } + return null; + } + + private Tuple InfoFromiOSAppUrl(string iosAppUrlString) + { + if(!UrlIsiOSApp(iosAppUrlString)) + { + return null; + } + var webUri = iosAppUrlString.Replace(Constants.iOSAppProtocol, string.Empty); + return new Tuple(webUri, null); + } + + private bool UrlIsMobileApp(string url) + { + return UrlIsAndroidApp(url) || UrlIsiOSApp(url); + } + + private bool UrlIsAndroidApp(string url) + { + return url.StartsWith(Constants.AndroidAppProtocol); + } + + private bool UrlIsiOSApp(string url) + { + return url.StartsWith(Constants.iOSAppProtocol); + } + + private Task EncryptObjPropertyAsync(V model, D obj, HashSet map, SymmetricCryptoKey key) + where V : View + where D : Domain + { + var modelType = model.GetType(); + var objType = obj.GetType(); + + async Task makeAndSetCs(string propName) + { + var modelPropInfo = modelType.GetProperty(propName); + var modelProp = modelPropInfo.GetValue(model) as string; + CipherString val = null; + if(!string.IsNullOrWhiteSpace(modelProp)) + { + val = await _cryptoService.EncryptAsync(modelProp, key); + } + var objPropInfo = objType.GetProperty(propName); + objPropInfo.SetValue(obj, val, null); + }; + + var tasks = new List(); + foreach(var prop in map) + { + tasks.Add(makeAndSetCs(prop)); + } + return Task.WhenAll(tasks); + } + + private async Task EncryptAttachmentsAsync(List attachmentsModel, SymmetricCryptoKey key, + Cipher cipher) + { + if(!attachmentsModel?.Any() ?? true) + { + cipher.Attachments = null; + return; + } + var tasks = new List(); + var encAttachments = new List(); + async Task encryptAndAddAttachmentAsync(AttachmentView model, Attachment attachment) + { + await EncryptObjPropertyAsync(model, attachment, new HashSet + { + "FileName" + }, key); + if(model.Key != null) + { + attachment.Key = await _cryptoService.EncryptAsync(model.Key.Key, key); + } + encAttachments.Add(attachment); + } + foreach(var model in attachmentsModel) + { + tasks.Add(encryptAndAddAttachmentAsync(model, new Attachment + { + Id = model.Id, + Size = model.Size, + SizeName = model.SizeName, + Url = model.Url + })); + } + await Task.WhenAll(tasks); + cipher.Attachments = encAttachments; + } + + private async Task EncryptCipherDataAsync(Cipher cipher, CipherView model, SymmetricCryptoKey key) + { + switch(cipher.Type) + { + case CipherType.Login: + cipher.Login = new Login + { + PasswordRevisionDate = model.Login.PasswordRevisionDate + }; + await EncryptObjPropertyAsync(model.Login, cipher.Login, new HashSet + { + "Username", + "Password", + "Totp" + }, key); + if(model.Login.Uris != null) + { + cipher.Login.Uris = new List(); + foreach(var uri in model.Login.Uris) + { + var loginUri = new LoginUri + { + Match = uri.Match + }; + await EncryptObjPropertyAsync(uri, loginUri, new HashSet { "Uri" }, key); + cipher.Login.Uris.Add(loginUri); + } + } + break; + case CipherType.SecureNote: + cipher.SecureNote = new SecureNote + { + Type = model.SecureNote.Type + }; + break; + case CipherType.Card: + cipher.Card = new Card(); + await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet + { + "CardholderName", + "Brand", + "Number", + "ExpMonth", + "ExpYear", + "Code" + }, key); + break; + case CipherType.Identity: + cipher.Identity = new Identity(); + await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet + { + "Title", + "FirstName", + "MiddleName", + "LastName", + "Address1", + "Address2", + "Address3", + "City", + "State", + "PostalCode", + "Country", + "Company", + "Email", + "Phone", + "SSN", + "Username", + "PassportNumber", + "LicenseNumber" + }, key); + break; + default: + throw new Exception("Unknown cipher type."); + } + } + + private async Task EncryptFieldsAsync(List fieldsModel, SymmetricCryptoKey key, + Cipher cipher) + { + if(!fieldsModel?.Any() ?? true) + { + cipher.Fields = null; + return; + } + var tasks = new List(); + var encFields = new List(); + async Task encryptAndAddFieldAsync(FieldView model, Field field) + { + await EncryptObjPropertyAsync(model, field, new HashSet + { + "Name", + "Value" + }, key); + encFields.Add(field); + } + foreach(var model in fieldsModel) + { + var field = new Field + { + Type = model.Type + }; + // normalize boolean type field values + if(model.Type == FieldType.Boolean && model.Value != "true") + { + model.Value = "false"; + } + tasks.Add(encryptAndAddFieldAsync(model, field)); + } + await Task.WhenAll(tasks); + cipher.Fields = encFields; + } + + private async Task EncryptPasswordHistoriesAsync(List phModels, + SymmetricCryptoKey key, Cipher cipher) + { + if(!phModels?.Any() ?? true) + { + cipher.PasswordHistory = null; + return; + } + var tasks = new List(); + var encPhs = new List(); + async Task encryptAndAddHistoryAsync(PasswordHistoryView model, PasswordHistory ph) + { + await EncryptObjPropertyAsync(model, ph, new HashSet + { + "Password" + }, key); + encPhs.Add(ph); + } + foreach(var model in phModels) + { + tasks.Add(encryptAndAddHistoryAsync(model, new PasswordHistory + { + LastUsedDate = model.LastUsedDate + })); + } + await Task.WhenAll(tasks); + cipher.PasswordHistory = encPhs; + } + + private class CipherLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public CipherLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(CipherView a, CipherView b) + { + var aName = a?.Name; + var bName = b?.Name; + if(aName == null && bName != null) + { + return -1; + } + if(aName != null && bName == null) + { + return 1; + } + if(aName == null && bName == null) + { + return 0; + } + var result = _i18nService.StringComparer.Compare(aName, bName); + if(result != 0 || a.Type != CipherType.Login || b.Type != CipherType.Login) + { + return result; + } + if(a.Login.Username != null) + { + aName += a.Login.Username; + } + if(b.Login.Username != null) + { + bName += b.Login.Username; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + + private class CipherLastUsedComparer : IComparer + { + public int Compare(CipherView a, CipherView b) + { + var aLastUsed = a.LocalData != null && a.LocalData.ContainsKey("lastUsedDate") ? + a.LocalData["lastUsedDate"] as DateTime? : null; + var bLastUsed = b.LocalData != null && b.LocalData.ContainsKey("lastUsedDate") ? + b.LocalData["lastUsedDate"] as DateTime? : null; + + var bothNotNull = aLastUsed != null && bLastUsed != null; + if(bothNotNull && aLastUsed.Value < bLastUsed.Value) + { + return 1; + } + if(aLastUsed != null && bLastUsed == null) + { + return -1; + } + if(bothNotNull && aLastUsed.Value > bLastUsed.Value) + { + return -1; + } + if(bLastUsed != null && aLastUsed == null) + { + return 1; + } + return 0; + } + } + + private class CipherLastUsedThenNameComparer : IComparer + { + private CipherLastUsedComparer _cipherLastUsedComparer; + private CipherLocaleComparer _cipherLocaleComparer; + + public CipherLastUsedThenNameComparer(II18nService i18nService) + { + _cipherLastUsedComparer = new CipherLastUsedComparer(); + _cipherLocaleComparer = new CipherLocaleComparer(i18nService); + } + + public int Compare(CipherView a, CipherView b) + { + var result = _cipherLastUsedComparer.Compare(a, b); + if(result != 0) + { + return result; + } + return _cipherLocaleComparer.Compare(a, b); + } + } + } +} diff --git a/src/Core/Services/CollectionService.cs b/src/Core/Services/CollectionService.cs new file mode 100644 index 000000000..317beec51 --- /dev/null +++ b/src/Core/Services/CollectionService.cs @@ -0,0 +1,244 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class CollectionService : ICollectionService + { + private const string Keys_CollectionsFormat = "collections_{0}"; + private const char NestingDelimiter = '/'; + + private List _decryptedCollectionCache; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IStorageService _storageService; + private readonly II18nService _i18nService; + + public CollectionService( + ICryptoService cryptoService, + IUserService userService, + IStorageService storageService, + II18nService i18nService) + { + _cryptoService = cryptoService; + _userService = userService; + _storageService = storageService; + _i18nService = i18nService; + } + + public void ClearCache() + { + _decryptedCollectionCache = null; + } + + public async Task EncryptAsync(CollectionView model) + { + if(model.OrganizationId == null) + { + throw new Exception("Collection has no organization id."); + } + var key = await _cryptoService.GetOrgKeyAsync(model.OrganizationId); + if(key == null) + { + throw new Exception("No key for this collection's organization."); + } + var collection = new Collection + { + Id = model.Id, + OrganizationId = model.OrganizationId, + ReadOnly = model.ReadOnly, + Name = await _cryptoService.EncryptAsync(model.Name, key) + }; + return collection; + } + + public async Task> DecryptManyAsync(List collections) + { + if(collections == null) + { + return new List(); + } + var decCollections = new List(); + async Task decryptAndAddCollectionAsync(Collection collection) + { + var c = await collection.DecryptAsync(); + decCollections.Add(c); + } + var tasks = new List(); + foreach(var collection in collections) + { + tasks.Add(decryptAndAddCollectionAsync(collection)); + } + await Task.WhenAll(tasks); + return decCollections.OrderBy(c => c, new CollectionLocaleComparer(_i18nService)).ToList(); + } + + public async Task GetAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var collections = await _storageService.GetAsync>( + string.Format(Keys_CollectionsFormat, userId)); + if(!collections?.ContainsKey(id) ?? true) + { + return null; + } + return new Collection(collections[id]); + } + + public async Task> GetAllAsync() + { + var userId = await _userService.GetUserIdAsync(); + var collections = await _storageService.GetAsync>( + string.Format(Keys_CollectionsFormat, userId)); + var response = collections?.Select(c => new Collection(c.Value)); + return response?.ToList() ?? new List(); + } + + // TODO: sequentialize? + public async Task> GetAllDecryptedAsync() + { + if(_decryptedCollectionCache != null) + { + return _decryptedCollectionCache; + } + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) + { + throw new Exception("No key."); + } + var collections = await GetAllAsync(); + _decryptedCollectionCache = await DecryptManyAsync(collections); + return _decryptedCollectionCache; + } + + public async Task>> GetAllNestedAsync(List collections = null) + { + if(collections == null) + { + collections = await GetAllDecryptedAsync(); + } + var nodes = new List>(); + foreach(var c in collections) + { + var collectionCopy = new CollectionView + { + Id = c.Id, + OrganizationId = c.OrganizationId + }; + CoreHelpers.NestedTraverse(nodes, 0, + Regex.Replace(c.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter), + collectionCopy, null, NestingDelimiter); + } + return nodes; + } + + public async Task> GetNestedAsync(string id) + { + var collections = await GetAllNestedAsync(); + return CoreHelpers.GetTreeNodeObject(collections, id); + } + + public async Task UpsertAsync(CollectionData collection) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(storageKey); + if(collections == null) + { + collections = new Dictionary(); + } + if(!collections.ContainsKey(collection.Id)) + { + collections.Add(collection.Id, null); + } + collections[collection.Id] = collection; + await _storageService.SaveAsync(storageKey, collections); + _decryptedCollectionCache = null; + } + + public async Task UpsertAsync(List collection) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(storageKey); + if(collections == null) + { + collections = new Dictionary(); + } + foreach(var c in collection) + { + if(!collections.ContainsKey(c.Id)) + { + collections.Add(c.Id, null); + } + collections[c.Id] = c; + } + await _storageService.SaveAsync(storageKey, collections); + _decryptedCollectionCache = null; + } + + public async Task ReplaceAsync(Dictionary collections) + { + var userId = await _userService.GetUserIdAsync(); + await _storageService.SaveAsync(string.Format(Keys_CollectionsFormat, userId), collections); + _decryptedCollectionCache = null; + } + + public async Task ClearAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_CollectionsFormat, userId)); + _decryptedCollectionCache = null; + } + + public async Task DeleteAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var collectionKey = string.Format(Keys_CollectionsFormat, userId); + var collections = await _storageService.GetAsync>(collectionKey); + if(collections == null || !collections.ContainsKey(id)) + { + return; + } + collections.Remove(id); + await _storageService.SaveAsync(collectionKey, collections); + _decryptedCollectionCache = null; + } + + private class CollectionLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public CollectionLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(CollectionView a, CollectionView b) + { + var aName = a?.Name; + var bName = b?.Name; + if(aName == null && bName != null) + { + return -1; + } + if(aName != null && bName == null) + { + return 1; + } + if(aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs new file mode 100644 index 000000000..4f546c955 --- /dev/null +++ b/src/Core/Services/CryptoService.cs @@ -0,0 +1,846 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Numerics; + +namespace Bit.Core.Services +{ + public class CryptoService : ICryptoService + { + private readonly IStorageService _storageService; + private readonly IStorageService _secureStorageService; + private readonly ICryptoFunctionService _cryptoFunctionService; + + private SymmetricCryptoKey _key; + private SymmetricCryptoKey _encKey; + private SymmetricCryptoKey _legacyEtmKey; + private string _keyHash; + private byte[] _publicKey; + private byte[] _privateKey; + private Dictionary _orgKeys; + private Task _getEncKeysTask; + private Task> _getOrgKeysTask; + + private const string Keys_Key = "key"; + private const string Keys_EncOrgKeys = "encOrgKeys"; + private const string Keys_EncPrivateKey = "encPrivateKey"; + private const string Keys_EncKey = "encKey"; + private const string Keys_KeyHash = "keyHash"; + + public CryptoService( + IStorageService storageService, + IStorageService secureStorageService, + ICryptoFunctionService cryptoFunctionService) + { + _storageService = storageService; + _secureStorageService = secureStorageService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task SetKeyAsync(SymmetricCryptoKey key) + { + _key = key; + var option = await _storageService.GetAsync(Constants.LockOptionKey); + var fingerprint = await _storageService.GetAsync(Constants.FingerprintUnlockKey); + if(option.HasValue && !fingerprint.GetValueOrDefault()) + { + // If we have a lock option set, we do not store the key + return; + } + await _secureStorageService.SaveAsync(Keys_Key, key.KeyB64); + } + + public async Task SetKeyHashAsync(string keyHash) + { + _keyHash = keyHash; + await _storageService.SaveAsync(Keys_KeyHash, keyHash); + } + + public async Task SetEncKeyAsync(string encKey) + { + if(encKey == null) + { + return; + } + await _storageService.SaveAsync(Keys_EncKey, encKey); + _encKey = null; + } + + public async Task SetEncPrivateKeyAsync(string encPrivateKey) + { + if(encPrivateKey == null) + { + return; + } + await _storageService.SaveAsync(Keys_EncPrivateKey, encPrivateKey); + _privateKey = null; + } + + public async Task SetOrgKeysAsync(IEnumerable orgs) + { + var orgKeys = orgs.ToDictionary(org => org.Id, org => org.Key); + _orgKeys = null; + await _storageService.SaveAsync(Keys_EncOrgKeys, orgKeys); + } + + public async Task GetKeyAsync() + { + if(_key != null) + { + return _key; + } + var key = await _secureStorageService.GetAsync(Keys_Key); + if(key != null) + { + _key = new SymmetricCryptoKey(Convert.FromBase64String(key)); + } + return _key; + } + + public async Task GetKeyHashAsync() + { + if(_keyHash != null) + { + return _keyHash; + } + var keyHash = await _storageService.GetAsync(Keys_KeyHash); + if(keyHash != null) + { + _keyHash = keyHash; + } + return _keyHash; + } + + public Task GetEncKeyAsync() + { + if(_encKey != null) + { + return Task.FromResult(_encKey); + } + if(_getEncKeysTask != null && !_getEncKeysTask.IsCompleted && !_getEncKeysTask.IsFaulted) + { + return _getEncKeysTask; + } + async Task doTask() + { + try + { + var encKey = await _storageService.GetAsync(Keys_EncKey); + if(encKey == null) + { + return null; + } + + var key = await GetKeyAsync(); + if(key == null) + { + return null; + } + + byte[] decEncKey = null; + var encKeyCipher = new CipherString(encKey); + if(encKeyCipher.EncryptionType == EncryptionType.AesCbc256_B64) + { + decEncKey = await DecryptToBytesAsync(encKeyCipher, key); + } + else if(encKeyCipher.EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64) + { + var newKey = await StretchKeyAsync(key); + decEncKey = await DecryptToBytesAsync(encKeyCipher, newKey); + } + else + { + throw new Exception("Unsupported encKey type."); + } + + if(decEncKey == null) + { + return null; + } + _encKey = new SymmetricCryptoKey(decEncKey); + return _encKey; + } + finally + { + _getEncKeysTask = null; + } + } + _getEncKeysTask = doTask(); + return _getEncKeysTask; + } + + public async Task GetPublicKeyAsync() + { + if(_publicKey != null) + { + return _publicKey; + } + var privateKey = await GetPrivateKeyAsync(); + if(privateKey == null) + { + return null; + } + _publicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(privateKey); + return _publicKey; + } + + public async Task GetPrivateKeyAsync() + { + if(_privateKey != null) + { + return _privateKey; + } + var encPrivateKey = await _storageService.GetAsync(Keys_EncPrivateKey); + if(encPrivateKey == null) + { + return null; + } + _privateKey = await DecryptToBytesAsync(new CipherString(encPrivateKey), null); + return _privateKey; + } + + public async Task> GetFingerprintAsync(string userId, byte[] publicKey = null) + { + if(publicKey == null) + { + publicKey = await GetPublicKeyAsync(); + } + if(publicKey == null) + { + throw new Exception("No public key available."); + } + var keyFingerprint = await _cryptoFunctionService.HashAsync(publicKey, CryptoHashAlgorithm.Sha256); + var userFingerprint = await HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32); + return HashPhrase(userFingerprint); + } + + public Task> GetOrgKeysAsync() + { + if(_orgKeys != null && _orgKeys.Count > 0) + { + return Task.FromResult(_orgKeys); + } + if(_getOrgKeysTask != null && !_getOrgKeysTask.IsCompleted && !_getOrgKeysTask.IsFaulted) + { + return _getOrgKeysTask; + } + async Task> doTask() + { + try + { + var encOrgKeys = await _storageService.GetAsync>(Keys_EncOrgKeys); + if(encOrgKeys == null) + { + return null; + } + var orgKeys = new Dictionary(); + var setKey = false; + foreach(var org in encOrgKeys) + { + var decValue = await RsaDecryptAsync(org.Value); + orgKeys.Add(org.Key, new SymmetricCryptoKey(decValue)); + setKey = true; + } + + if(setKey) + { + _orgKeys = orgKeys; + } + return _orgKeys; + } + finally + { + _getOrgKeysTask = null; + } + } + _getOrgKeysTask = doTask(); + return _getOrgKeysTask; + } + + public async Task GetOrgKeyAsync(string orgId) + { + if(string.IsNullOrWhiteSpace(orgId)) + { + return null; + } + var orgKeys = await GetOrgKeysAsync(); + if(orgKeys == null || !orgKeys.ContainsKey(orgId)) + { + return null; + } + return orgKeys[orgId]; + } + + public async Task HasKeyAsync() + { + var key = await GetKeyAsync(); + return key != null; + } + + public async Task HasEncKeyAsync() + { + var encKey = await _storageService.GetAsync(Keys_EncKey); + return encKey != null; + } + + public async Task ClearKeyAsync() + { + _key = _legacyEtmKey = null; + await _secureStorageService.RemoveAsync(Keys_Key); + } + + public async Task ClearKeyHashAsync() + { + _keyHash = null; + await _storageService.RemoveAsync(Keys_KeyHash); + } + + public async Task ClearEncKeyAsync(bool memoryOnly = false) + { + _encKey = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncKey); + } + } + + public async Task ClearKeyPairAsync(bool memoryOnly = false) + { + _publicKey = _privateKey = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncPrivateKey); + } + } + + public async Task ClearOrgKeysAsync(bool memoryOnly = false) + { + _orgKeys = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncOrgKeys); + } + } + + public async Task ClearPinProtectedKeyAsync() + { + await _storageService.RemoveAsync(Constants.PinProtectedKey); + } + + public async Task ClearKeysAsync() + { + await Task.WhenAll(new Task[] + { + ClearKeyAsync(), + ClearKeyHashAsync(), + ClearOrgKeysAsync(), + ClearEncKeyAsync(), + ClearKeyPairAsync(), + ClearPinProtectedKeyAsync() + }); + } + + public async Task ToggleKeyAsync() + { + var key = await GetKeyAsync(); + var option = await _storageService.GetAsync(Constants.LockOptionKey); + var fingerprint = await _storageService.GetAsync(Constants.FingerprintUnlockKey); + if(!fingerprint.GetValueOrDefault() && (option != null || option == 0)) + { + await ClearKeyAsync(); + _key = key; + return; + } + await SetKeyAsync(key); + } + + public async Task MakeKeyAsync(string password, string salt, + KdfType? kdf, int? kdfIterations) + { + byte[] key = null; + if(kdf == null || kdf == KdfType.PBKDF2_SHA256) + { + if(kdfIterations == null) + { + kdfIterations = 5000; + } + if(kdfIterations < 5000) + { + throw new Exception("PBKDF2 iteration minimum is 5000."); + } + key = await _cryptoFunctionService.Pbkdf2Async(password, salt, + CryptoHashAlgorithm.Sha256, kdfIterations.Value); + } + else + { + throw new Exception("Unknown kdf."); + } + return new SymmetricCryptoKey(key); + } + + public async Task MakeKeyFromPinAsync(string pin, string salt, + KdfType kdf, int kdfIterations) + { + var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + if(pinProtectedKey == null) + { + throw new Exception("No PIN protected key found."); + } + var protectedKeyCs = new CipherString(pinProtectedKey); + var pinKey = await MakePinKeyAysnc(pin, salt, kdf, kdfIterations); + var decKey = await DecryptToBytesAsync(protectedKeyCs, pinKey); + return new SymmetricCryptoKey(decKey); + } + + public async Task> MakeShareKeyAsync() + { + var shareKey = await _cryptoFunctionService.RandomBytesAsync(64); + var publicKey = await GetPublicKeyAsync(); + var encShareKey = await RsaEncryptAsync(shareKey, publicKey); + return new Tuple(encShareKey, new SymmetricCryptoKey(shareKey)); + } + + public async Task> MakeKeyPairAsync(SymmetricCryptoKey key = null) + { + var keyPair = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048); + var publicB64 = Convert.ToBase64String(keyPair.Item1); + var privateEnc = await EncryptAsync(keyPair.Item2, key); + return new Tuple(publicB64, privateEnc); + } + + public async Task MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations) + { + var pinKey = await MakeKeyAsync(pin, salt, kdf, kdfIterations); + return await StretchKeyAsync(pinKey); + } + + public async Task HashPasswordAsync(string password, SymmetricCryptoKey key) + { + if(key == null) + { + key = await GetKeyAsync(); + } + if(password == null || key == null) + { + throw new Exception("Invalid parameters."); + } + var hash = await _cryptoFunctionService.Pbkdf2Async(key.Key, password, CryptoHashAlgorithm.Sha256, 1); + return Convert.ToBase64String(hash); + } + + public async Task> MakeEncKeyAsync(SymmetricCryptoKey key) + { + var theKey = await GetKeyForEncryptionAsync(key); + var encKey = await _cryptoFunctionService.RandomBytesAsync(64); + return await BuildEncKeyAsync(theKey, encKey); + } + + public async Task> RemakeEncKeyAsync(SymmetricCryptoKey key) + { + var encKey = await GetEncKeyAsync(); + return await BuildEncKeyAsync(key, encKey.Key); + } + + public async Task EncryptAsync(string plainValue, SymmetricCryptoKey key = null) + { + if(plainValue == null) + { + return null; + } + return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key); + } + + public async Task EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + if(plainValue == null) + { + return null; + } + var encObj = await AesEncryptAsync(plainValue, key); + var iv = Convert.ToBase64String(encObj.Iv); + var data = Convert.ToBase64String(encObj.Data); + var mac = encObj.Mac != null ? Convert.ToBase64String(encObj.Mac) : null; + return new CipherString(encObj.Key.EncType, data, iv, mac); + } + + public async Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + var encValue = await AesEncryptAsync(plainValue, key); + var macLen = 0; + if(encValue.Mac != null) + { + macLen = encValue.Mac.Length; + } + var encBytes = new byte[1 + encValue.Iv.Length + macLen + encValue.Data.Length]; + Buffer.BlockCopy(new byte[] { (byte)encValue.Key.EncType }, 0, encBytes, 0, 1); + Buffer.BlockCopy(encValue.Iv, 0, encBytes, 1, encValue.Iv.Length); + if(encValue.Mac != null) + { + Buffer.BlockCopy(encValue.Mac, 0, encBytes, 1 + encValue.Iv.Length, encValue.Mac.Length); + } + Buffer.BlockCopy(encValue.Data, 0, encBytes, 1 + encValue.Iv.Length + macLen, encValue.Data.Length); + return encBytes; + } + + public async Task RsaEncryptAsync(byte[] data, byte[] publicKey = null) + { + if(publicKey == null) + { + publicKey = await GetPublicKeyAsync(); + } + if(publicKey == null) + { + throw new Exception("Public key unavailable."); + } + var encBytes = await _cryptoFunctionService.RsaEncryptAsync(data, publicKey, CryptoHashAlgorithm.Sha1); + return new CipherString(EncryptionType.Rsa2048_OaepSha1_B64, Convert.ToBase64String(encBytes)); + } + + public async Task DecryptToBytesAsync(CipherString cipherString, SymmetricCryptoKey key = null) + { + var iv = Convert.FromBase64String(cipherString.Iv); + var data = Convert.FromBase64String(cipherString.Data); + var mac = !string.IsNullOrWhiteSpace(cipherString.Mac) ? Convert.FromBase64String(cipherString.Mac) : null; + return await AesDecryptToBytesAsync(cipherString.EncryptionType, data, iv, mac, key); + } + + public async Task DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null) + { + return await AesDecryptToUtf8Async(cipherString.EncryptionType, cipherString.Data, + cipherString.Iv, cipherString.Mac, key); + } + + public async Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key) + { + if(encBytes == null) + { + throw new Exception("no encBytes."); + } + + var encType = (EncryptionType)encBytes[0]; + byte[] ctBytes = null; + byte[] ivBytes = null; + byte[] macBytes = null; + + switch(encType) + { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if(encBytes.Length < 49) // 1 + 16 + 32 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + macBytes = new ArraySegment(encBytes, 17, 32).ToArray(); + ctBytes = new ArraySegment(encBytes, 49, encBytes.Length - 49).ToArray(); + break; + case EncryptionType.AesCbc256_B64: + if(encBytes.Length < 17) // 1 + 16 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + ctBytes = new ArraySegment(encBytes, 17, encBytes.Length - 17).ToArray(); + break; + default: + return null; + } + + return await AesDecryptToBytesAsync(encType, ctBytes, ivBytes, macBytes, key); + } + + public async Task RandomNumberAsync(int min, int max) + { + // Make max inclusive + max = max + 1; + + var diff = (long)max - min; + var upperBound = uint.MaxValue / diff * diff; + uint ui; + do + { + ui = await _cryptoFunctionService.RandomNumberAsync(); + } while(ui >= upperBound); + return (int)(min + (ui % diff)); + } + + // Helpers + + private async Task AesEncryptAsync(byte[] data, SymmetricCryptoKey key) + { + var obj = new EncryptedObject + { + Key = await GetKeyForEncryptionAsync(key), + Iv = await _cryptoFunctionService.RandomBytesAsync(16) + }; + obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey); + if(obj.Key.MacKey != null) + { + var macData = new byte[obj.Iv.Length + obj.Data.Length]; + Buffer.BlockCopy(obj.Iv, 0, macData, 0, obj.Iv.Length); + Buffer.BlockCopy(obj.Data, 0, macData, obj.Iv.Length, obj.Data.Length); + obj.Mac = await _cryptoFunctionService.HmacAsync(macData, obj.Key.MacKey, CryptoHashAlgorithm.Sha256); + } + return obj; + } + + private async Task AesDecryptToUtf8Async(EncryptionType encType, string data, string iv, string mac, + SymmetricCryptoKey key) + { + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if(theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if(theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // "Fast params" conversion + var encKey = theKey.EncKey; + var dataBytes = Convert.FromBase64String(data); + var ivBytes = Convert.FromBase64String(iv); + + var macDataBytes = new byte[ivBytes.Length + dataBytes.Length]; + Buffer.BlockCopy(ivBytes, 0, macDataBytes, 0, ivBytes.Length); + Buffer.BlockCopy(dataBytes, 0, macDataBytes, ivBytes.Length, dataBytes.Length); + + byte[] macKey = null; + if(theKey.MacKey != null) + { + macKey = theKey.MacKey; + } + byte[] macBytes = null; + if(mac != null) + { + macBytes = Convert.FromBase64String(mac); + } + + // Compute mac + if(macKey != null && macBytes != null) + { + var computedMac = await _cryptoFunctionService.HmacAsync(macDataBytes, macKey, + CryptoHashAlgorithm.Sha256); + var macsEqual = await _cryptoFunctionService.CompareAsync(macBytes, computedMac); + if(!macsEqual) + { + // Mac failed + return null; + } + } + + var decBytes = await _cryptoFunctionService.AesDecryptAsync(dataBytes, ivBytes, encKey); + return Encoding.UTF8.GetString(decBytes); + } + + private async Task AesDecryptToBytesAsync(EncryptionType encType, byte[] data, byte[] iv, byte[] mac, + SymmetricCryptoKey key) + { + + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if(theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if(theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // Compute mac + if(theKey.MacKey != null && mac != null) + { + var macData = new byte[iv.Length + data.Length]; + Buffer.BlockCopy(iv, 0, macData, 0, iv.Length); + Buffer.BlockCopy(data, 0, macData, iv.Length, data.Length); + + var computedMac = await _cryptoFunctionService.HmacAsync(macData, theKey.MacKey, + CryptoHashAlgorithm.Sha256); + if(computedMac == null) + { + return null; + } + var macsMatch = await _cryptoFunctionService.CompareAsync(mac, computedMac); + if(!macsMatch) + { + // Mac failed + return null; + } + } + + return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey); + } + + private async Task RsaDecryptAsync(string encValue) + { + var headerPieces = encValue.Split('.'); + EncryptionType? encType = null; + string[] encPieces = null; + + if(headerPieces.Length == 1) + { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = new string[] { headerPieces[0] }; + } + else if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out EncryptionType type)) + { + encType = type; + encPieces = headerPieces[1].Split('|'); + } + + if(!encType.HasValue) + { + throw new Exception("encType unavailable."); + } + if(encPieces == null || encPieces.Length == 0) + { + throw new Exception("encPieces unavailable."); + } + + var data = Convert.FromBase64String(encPieces[0]); + var privateKey = await GetPrivateKeyAsync(); + if(privateKey == null) + { + throw new Exception("No private key."); + } + + var alg = CryptoHashAlgorithm.Sha1; + switch(encType.Value) + { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = CryptoHashAlgorithm.Sha256; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Exception("encType unavailable."); + } + + return await _cryptoFunctionService.RsaDecryptAsync(data, privateKey, alg); + } + + private async Task GetKeyForEncryptionAsync(SymmetricCryptoKey key = null) + { + if(key != null) + { + return key; + } + var encKey = await GetEncKeyAsync(); + if(encKey != null) + { + return encKey; + } + return await GetKeyAsync(); + } + + private SymmetricCryptoKey ResolveLegacyKey(EncryptionType encKey, SymmetricCryptoKey key) + { + if(encKey == EncryptionType.AesCbc128_HmacSha256_B64 && key.EncType == EncryptionType.AesCbc256_B64) + { + // Old encrypt-then-mac scheme, make a new key + if(_legacyEtmKey == null) + { + _legacyEtmKey = new SymmetricCryptoKey(key.Key, EncryptionType.AesCbc128_HmacSha256_B64); + } + return _legacyEtmKey; + } + return key; + } + + private async Task StretchKeyAsync(SymmetricCryptoKey key) + { + var newKey = new byte[64]; + var enc = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32); + Buffer.BlockCopy(enc, 0, newKey, 0, 32); + var mac = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32); + Buffer.BlockCopy(mac, 0, newKey, 32, 32); + return new SymmetricCryptoKey(newKey); + } + + // ref: https://tools.ietf.org/html/rfc5869 + private async Task HkdfExpandAsync(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 = await _cryptoFunctionService.HmacAsync(t, prk, CryptoHashAlgorithm.Sha256); + previousT.CopyTo(okm, i * hashLen); + } + return okm; + } + + private List HashPhrase(byte[] hash, int minimumEntropy = 64) + { + var wordLength = Utilities.WordList.EEFLongWordList.Count; + var entropyPerWord = Math.Log(wordLength) / Math.Log(2); + var numWords = (int)Math.Ceiling(minimumEntropy / entropyPerWord); + + var entropyAvailable = hash.Length * 4; + if(numWords * entropyPerWord > entropyAvailable) + { + throw new Exception("Output entropy of hash function is too small"); + } + + var phrase = new List(); + var hashHex = string.Concat("0", BitConverter.ToString(hash).Replace("-", "")); + var hashNumber = BigInteger.Parse(hashHex, System.Globalization.NumberStyles.HexNumber); + while(numWords-- > 0) + { + var remainder = (int)(hashNumber % wordLength); + hashNumber = hashNumber / wordLength; + phrase.Add(Utilities.WordList.EEFLongWordList[remainder]); + } + return phrase; + } + + private async Task> BuildEncKeyAsync(SymmetricCryptoKey key, + byte[] encKey) + { + CipherString encKeyEnc = null; + if(key.Key.Length == 32) + { + var newKey = await StretchKeyAsync(key); + encKeyEnc = await EncryptAsync(encKey, newKey); + } + else if(key.Key.Length == 64) + { + encKeyEnc = await EncryptAsync(encKey, key); + } + else + { + throw new Exception("Invalid key size."); + } + return new Tuple(new SymmetricCryptoKey(encKey), encKeyEnc); + } + + private class EncryptedObject + { + public byte[] Iv { get; set; } + public byte[] Data { get; set; } + public byte[] Mac { get; set; } + public SymmetricCryptoKey Key { get; set; } + } + } +} diff --git a/src/Core/Services/EnvironmentService.cs b/src/Core/Services/EnvironmentService.cs new file mode 100644 index 000000000..68eb6b8d0 --- /dev/null +++ b/src/Core/Services/EnvironmentService.cs @@ -0,0 +1,110 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class EnvironmentService : IEnvironmentService + { + private readonly IApiService _apiService; + private readonly IStorageService _storageService; + + public EnvironmentService( + IApiService apiService, + IStorageService storageService) + { + _apiService = apiService; + _storageService = storageService; + } + + public string BaseUrl { get; set; } + public string WebVaultUrl { get; set; } + public string ApiUrl { get; set; } + public string IdentityUrl { get; set; } + public string IconsUrl { get; set; } + public string NotificationsUrl { get; set; } + + public string GetWebVaultUrl() + { + if(!string.IsNullOrWhiteSpace(WebVaultUrl)) + { + return WebVaultUrl; + } + else if(string.IsNullOrWhiteSpace(BaseUrl)) + { + return BaseUrl; + } + return null; + } + + public async Task SetUrlsFromStorageAsync() + { + var urls = await _storageService.GetAsync(Constants.EnvironmentUrlsKey); + if(urls == null) + { + urls = new EnvironmentUrlData(); + } + var envUrls = new EnvironmentUrls(); + if(!string.IsNullOrWhiteSpace(urls.Base)) + { + BaseUrl = envUrls.Base = urls.Base; + _apiService.SetUrls(envUrls); + return; + } + WebVaultUrl = urls.WebVault; + ApiUrl = envUrls.Api = urls.Api; + IdentityUrl = envUrls.Identity = urls.Identity; + IconsUrl = urls.Icons; + NotificationsUrl = urls.Notifications; + _apiService.SetUrls(envUrls); + } + + public async Task SetUrlsAsync(EnvironmentUrlData urls) + { + urls.Base = FormatUrl(urls.Base); + urls.WebVault = FormatUrl(urls.WebVault); + urls.Api = FormatUrl(urls.Api); + urls.Identity = FormatUrl(urls.Identity); + urls.Icons = FormatUrl(urls.Icons); + urls.Notifications = FormatUrl(urls.Notifications); + await _storageService.SaveAsync(Constants.EnvironmentUrlsKey, urls); + BaseUrl = urls.Base; + WebVaultUrl = urls.WebVault; + ApiUrl = urls.Api; + IdentityUrl = urls.Identity; + IconsUrl = urls.Icons; + NotificationsUrl = urls.Notifications; + + var envUrls = new EnvironmentUrls(); + if(!string.IsNullOrWhiteSpace(BaseUrl)) + { + envUrls.Base = BaseUrl; + } + else + { + envUrls.Api = ApiUrl; + envUrls.Identity = IdentityUrl; + } + + _apiService.SetUrls(envUrls); + // TODO: init notifications service + return urls; + } + + private string FormatUrl(string url) + { + if(string.IsNullOrWhiteSpace(url)) + { + return null; + } + url = Regex.Replace(url, "\\/+$", string.Empty); + if(!url.StartsWith("http://") && !url.StartsWith("https://")) + { + url = string.Concat("https://", url); + } + return url.Trim(); + } + } +} diff --git a/src/Core/Services/FolderService.cs b/src/Core/Services/FolderService.cs new file mode 100644 index 000000000..9543d2e28 --- /dev/null +++ b/src/Core/Services/FolderService.cs @@ -0,0 +1,282 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class FolderService : IFolderService + { + private const string Keys_CiphersFormat = "ciphers_{0}"; + private const string Keys_FoldersFormat = "folders_{0}"; + private const char NestingDelimiter = '/'; + + private List _decryptedFolderCache; + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IApiService _apiService; + private readonly IStorageService _storageService; + private readonly II18nService _i18nService; + private readonly ICipherService _cipherService; + + public FolderService( + ICryptoService cryptoService, + IUserService userService, + IApiService apiService, + IStorageService storageService, + II18nService i18nService, + ICipherService cipherService) + { + _cryptoService = cryptoService; + _userService = userService; + _apiService = apiService; + _storageService = storageService; + _i18nService = i18nService; + _cipherService = cipherService; + } + + public void ClearCache() + { + _decryptedFolderCache = null; + } + + public async Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null) + { + var folder = new Folder + { + Id = model.Id, + Name = await _cryptoService.EncryptAsync(model.Name, key) + }; + return folder; + } + + public async Task GetAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var folders = await _storageService.GetAsync>( + string.Format(Keys_FoldersFormat, userId)); + if(!folders?.ContainsKey(id) ?? true) + { + return null; + } + return new Folder(folders[id]); + } + + public async Task> GetAllAsync() + { + var userId = await _userService.GetUserIdAsync(); + var folders = await _storageService.GetAsync>( + string.Format(Keys_FoldersFormat, userId)); + var response = folders?.Select(f => new Folder(f.Value)); + return response?.ToList() ?? new List(); + } + + // TODO: sequentialize? + public async Task> GetAllDecryptedAsync() + { + if(_decryptedFolderCache != null) + { + return _decryptedFolderCache; + } + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) + { + throw new Exception("No key."); + } + var decFolders = new List(); + async Task decryptAndAddFolderAsync(Folder folder) + { + var f = await folder.DecryptAsync(); + decFolders.Add(f); + } + var tasks = new List(); + var folders = await GetAllAsync(); + foreach(var folder in folders) + { + tasks.Add(decryptAndAddFolderAsync(folder)); + } + await Task.WhenAll(tasks); + decFolders = decFolders.OrderBy(f => f, new FolderLocaleComparer(_i18nService)).ToList(); + + var noneFolder = new FolderView + { + Name = _i18nService.T("FolderNone") + }; + decFolders.Add(noneFolder); + + _decryptedFolderCache = decFolders; + return _decryptedFolderCache; + } + + public async Task>> GetAllNestedAsync() + { + var folders = await GetAllDecryptedAsync(); + var nodes = new List>(); + foreach(var f in folders) + { + var folderCopy = new FolderView + { + Id = f.Id, + RevisionDate = f.RevisionDate + }; + CoreHelpers.NestedTraverse(nodes, 0, + Regex.Replace(f.Name, "^\\/+|\\/+$", string.Empty).Split(NestingDelimiter), + folderCopy, null, NestingDelimiter); + } + return nodes; + } + + public async Task> GetNestedAsync(string id) + { + var folders = await GetAllNestedAsync(); + return CoreHelpers.GetTreeNodeObject(folders, id); + } + + public async Task SaveWithServerAsync(Folder folder) + { + var request = new FolderRequest(folder); + FolderResponse response; + if(folder.Id == null) + { + response = await _apiService.PostFolderAsync(request); + folder.Id = response.Id; + } + else + { + response = await _apiService.PutFolderAsync(folder.Id, request); + } + var userId = await _userService.GetUserIdAsync(); + var data = new FolderData(response, userId); + await UpsertAsync(data); + } + + public async Task UpsertAsync(FolderData folder) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_FoldersFormat, userId); + var folders = await _storageService.GetAsync>(storageKey); + if(folders == null) + { + folders = new Dictionary(); + } + if(!folders.ContainsKey(folder.Id)) + { + folders.Add(folder.Id, null); + } + folders[folder.Id] = folder; + await _storageService.SaveAsync(storageKey, folders); + _decryptedFolderCache = null; + } + + public async Task UpsertAsync(List folder) + { + var userId = await _userService.GetUserIdAsync(); + var storageKey = string.Format(Keys_FoldersFormat, userId); + var folders = await _storageService.GetAsync>(storageKey); + if(folders == null) + { + folders = new Dictionary(); + } + foreach(var f in folder) + { + if(!folders.ContainsKey(f.Id)) + { + folders.Add(f.Id, null); + } + folders[f.Id] = f; + } + await _storageService.SaveAsync(storageKey, folders); + _decryptedFolderCache = null; + } + + public async Task ReplaceAsync(Dictionary folders) + { + var userId = await _userService.GetUserIdAsync(); + await _storageService.SaveAsync(string.Format(Keys_FoldersFormat, userId), folders); + _decryptedFolderCache = null; + } + + public async Task ClearAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_FoldersFormat, userId)); + _decryptedFolderCache = null; + } + + public async Task DeleteAsync(string id) + { + var userId = await _userService.GetUserIdAsync(); + var folderKey = string.Format(Keys_FoldersFormat, userId); + var folders = await _storageService.GetAsync>(folderKey); + if(folders == null || !folders.ContainsKey(id)) + { + return; + } + folders.Remove(id); + await _storageService.SaveAsync(folderKey, folders); + _decryptedFolderCache = null; + + // Items in a deleted folder are re-assigned to "No Folder" + var ciphers = await _storageService.GetAsync>( + string.Format(Keys_CiphersFormat, userId)); + if(ciphers != null) + { + var updates = new List(); + foreach(var c in ciphers) + { + if(c.Value.FolderId == id) + { + c.Value.FolderId = null; + updates.Add(c.Value); + } + } + if(updates.Any()) + { + await _cipherService.UpsertAsync(updates); + } + } + } + + public async Task DeleteWithServerAsync(string id) + { + await _apiService.DeleteFolderAsync(id); + await DeleteAsync(id); + } + + private class FolderLocaleComparer : IComparer + { + private readonly II18nService _i18nService; + + public FolderLocaleComparer(II18nService i18nService) + { + _i18nService = i18nService; + } + + public int Compare(FolderView a, FolderView b) + { + var aName = a?.Name; + var bName = b?.Name; + if(aName == null && bName != null) + { + return -1; + } + if(aName != null && bName == null) + { + return 1; + } + if(aName == null && bName == null) + { + return 0; + } + return _i18nService.StringComparer.Compare(aName, bName); + } + } + } +} diff --git a/src/Core/Services/LiteDbStorageService.cs b/src/Core/Services/LiteDbStorageService.cs new file mode 100644 index 000000000..d0b836673 --- /dev/null +++ b/src/Core/Services/LiteDbStorageService.cs @@ -0,0 +1,88 @@ +using Bit.Core.Abstractions; +using LiteDB; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class LiteDbStorageService : IStorageService + { + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + private readonly string _dbPath; + private LiteCollection _collection; + private Task _initTask; + + public LiteDbStorageService(string dbPath) + { + _dbPath = dbPath; + } + + public Task InitAsync() + { + if(_collection != null) + { + return Task.FromResult(0); + } + if(_initTask != null) + { + return _initTask; + } + _initTask = Task.Run(() => + { + try + { + var db = new LiteDatabase($"Filename={_dbPath};"); + _collection = db.GetCollection("json_items"); + } + finally + { + _initTask = null; + } + }); + return _initTask; + } + + public async Task GetAsync(string key) + { + await InitAsync(); + var item = _collection.Find(i => i.Id == key).FirstOrDefault(); + if(item == null) + { + return default(T); + } + return JsonConvert.DeserializeObject(item.Value, _jsonSettings); + } + + public async Task SaveAsync(string key, T obj) + { + await InitAsync(); + var data = JsonConvert.SerializeObject(obj, _jsonSettings); + _collection.Upsert(new JsonItem(key, data)); + } + + public async Task RemoveAsync(string key) + { + await InitAsync(); + _collection.Delete(i => i.Id == key); + } + + private class JsonItem + { + public JsonItem() { } + + public JsonItem(string key, string value) + { + Id = key; + Value = value; + } + + public string Id { get; set; } + public string Value { get; set; } + } + } +} diff --git a/src/Core/Services/LockService.cs b/src/Core/Services/LockService.cs new file mode 100644 index 000000000..209598a2d --- /dev/null +++ b/src/Core/Services/LockService.cs @@ -0,0 +1,167 @@ +using Bit.Core.Abstractions; +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class LockService : ILockService + { + private readonly ICryptoService _cryptoService; + private readonly IUserService _userService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IStorageService _storageService; + private readonly IFolderService _folderService; + private readonly ICipherService _cipherService; + private readonly ICollectionService _collectionService; + private readonly ISearchService _searchService; + private readonly IMessagingService _messagingService; + + public LockService( + ICryptoService cryptoService, + IUserService userService, + IPlatformUtilsService platformUtilsService, + IStorageService storageService, + IFolderService folderService, + ICipherService cipherService, + ICollectionService collectionService, + ISearchService searchService, + IMessagingService messagingService) + { + _cryptoService = cryptoService; + _userService = userService; + _platformUtilsService = platformUtilsService; + _storageService = storageService; + _folderService = folderService; + _cipherService = cipherService; + _collectionService = collectionService; + _searchService = searchService; + _messagingService = messagingService; + } + + public bool PinLocked { get; set; } + public bool FingerprintLocked { get; set; } = true; + + // TODO: init timer? + + public async Task IsLockedAsync() + { + var hasKey = await _cryptoService.HasKeyAsync(); + if(hasKey) + { + if(PinLocked) + { + return true; + } + else + { + var fingerprintSet = await IsFingerprintLockSetAsync(); + if(fingerprintSet && FingerprintLocked) + { + return true; + } + } + } + return !hasKey; + } + + public async Task CheckLockAsync() + { + if(false) // TODO: view is open? + { + return; + } + var authed = await _userService.IsAuthenticatedAsync(); + if(!authed) + { + return; + } + if(await IsLockedAsync()) + { + return; + } + var lockOption = _platformUtilsService.LockTimeout(); + if(lockOption == null) + { + lockOption = await _storageService.GetAsync(Constants.LockOptionKey); + } + if(lockOption.GetValueOrDefault(-1) < 0) + { + return; + } + var lastActive = await _storageService.GetAsync(Constants.LastActiveKey); + if(lastActive == null) + { + return; + } + var diff = DateTime.UtcNow - lastActive.Value; + if(diff.TotalSeconds >= lockOption.Value) + { + // need to lock now + await LockAsync(true); + } + } + + public async Task LockAsync(bool allowSoftLock = false) + { + var authed = await _userService.IsAuthenticatedAsync(); + if(!authed) + { + return; + } + if(allowSoftLock) + { + var pinSet = await IsPinLockSetAsync(); + if(pinSet.Item1) + { + PinLocked = true; + } + if(await IsFingerprintLockSetAsync()) + { + FingerprintLocked = true; + } + if(FingerprintLocked || PinLocked) + { + _messagingService.Send("locked"); + // TODO: locked callback? + return; + } + } + await Task.WhenAll( + _cryptoService.ClearKeyAsync(), + _cryptoService.ClearOrgKeysAsync(true), + _cryptoService.ClearKeyPairAsync(true), + _cryptoService.ClearEncKeyAsync(true)); + + _folderService.ClearCache(); + _cipherService.ClearCache(); + _collectionService.ClearCache(); + _searchService.ClearIndex(); + _messagingService.Send("locked"); + // TODO: locked callback? + } + + public async Task SetLockOptionAsync(int? lockOption) + { + await _storageService.SaveAsync(Constants.LockOptionKey, lockOption); + await _cryptoService.ToggleKeyAsync(); + } + + public async Task> IsPinLockSetAsync() + { + var protectedPin = await _storageService.GetAsync(Constants.ProtectedPin); + var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + return new Tuple(protectedPin != null, pinProtectedKey != null); + } + + public async Task IsFingerprintLockSetAsync() + { + var fingerprintLock = await _storageService.GetAsync(Constants.FingerprintUnlockKey); + return fingerprintLock.GetValueOrDefault(); + } + + public async Task ClearAsync() + { + await _storageService.RemoveAsync(Constants.ProtectedPin); + } + } +} diff --git a/src/Core/Services/PasswordGenerationService.cs b/src/Core/Services/PasswordGenerationService.cs new file mode 100644 index 000000000..f7c95d080 --- /dev/null +++ b/src/Core/Services/PasswordGenerationService.cs @@ -0,0 +1,393 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class PasswordGenerationService : IPasswordGenerationService + { + private const string Keys_Options = "passwordGenerationOptions"; + private const string Keys_History = "generatedPasswordHistory"; + private const int MaxPasswordsInHistory = 100; + private const string LowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; + private const string UppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + private const string NumberCharSet = "23456789"; + private const string SpecialCharSet = "!@#$%^&*"; + + private readonly ICryptoService _cryptoService; + private readonly IStorageService _storageService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true); + private PasswordGenerationOptions _optionsCache; + private List _history; + + public PasswordGenerationService( + ICryptoService cryptoService, + IStorageService storageService, + ICryptoFunctionService cryptoFunctionService) + { + _cryptoService = cryptoService; + _storageService = storageService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task GeneratePasswordAsync(PasswordGenerationOptions options) + { + // Overload defaults with given options + options.Merge(_defaultOptions); + if(options.Type == "passphrase") + { + return await GeneratePassphraseAsync(options); + } + + // Sanitize + if(options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() <= 0) + { + options.MinUppercase = 1; + } + if(options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() <= 0) + { + options.MinLowercase = 1; + } + if(options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() <= 0) + { + options.MinNumber = 1; + } + if(options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() <= 0) + { + options.MinSpecial = 1; + } + + if(options.Length.GetValueOrDefault() < 1) + { + options.Length = 10; + } + var minLength = options.MinSpecial.GetValueOrDefault() + options.MinLowercase.GetValueOrDefault() + + options.MinNumber.GetValueOrDefault() + options.MinSpecial.GetValueOrDefault(); + if(options.Length < minLength) + { + options.Length = minLength; + } + + var positionsBuilder = new StringBuilder(); + if(options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() > 0) + { + for(int i = 0; i < options.MinLowercase.GetValueOrDefault(); i++) + { + positionsBuilder.Append("l"); + } + } + if(options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() > 0) + { + for(int i = 0; i < options.MinUppercase.GetValueOrDefault(); i++) + { + positionsBuilder.Append("u"); + } + } + if(options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() > 0) + { + for(int i = 0; i < options.MinNumber.GetValueOrDefault(); i++) + { + positionsBuilder.Append("n"); + } + } + if(options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() > 0) + { + for(int i = 0; i < options.MinSpecial.GetValueOrDefault(); i++) + { + positionsBuilder.Append("s"); + } + } + while(positionsBuilder.Length < options.Length.GetValueOrDefault()) + { + positionsBuilder.Append("a"); + } + + // Shuffle + var positions = positionsBuilder.ToString().ToCharArray() + .OrderBy(a => _cryptoFunctionService.RandomNumber()).ToArray(); + + // Build out other character sets + var allCharSet = string.Empty; + var lowercaseCharSet = LowercaseCharSet; + if(options.Ambiguous.GetValueOrDefault()) + { + lowercaseCharSet = string.Concat(lowercaseCharSet, "l"); + } + if(options.Lowercase.GetValueOrDefault()) + { + allCharSet = string.Concat(allCharSet, lowercaseCharSet); + } + + var uppercaseCharSet = UppercaseCharSet; + if(options.Ambiguous.GetValueOrDefault()) + { + uppercaseCharSet = string.Concat(uppercaseCharSet, "IO"); + } + if(options.Uppercase.GetValueOrDefault()) + { + allCharSet = string.Concat(allCharSet, uppercaseCharSet); + } + + var numberCharSet = NumberCharSet; + if(options.Ambiguous.GetValueOrDefault()) + { + numberCharSet = string.Concat(numberCharSet, "01"); + } + if(options.Number.GetValueOrDefault()) + { + allCharSet = string.Concat(allCharSet, numberCharSet); + } + + var specialCharSet = SpecialCharSet; + if(options.Special.GetValueOrDefault()) + { + allCharSet = string.Concat(allCharSet, specialCharSet); + } + + var password = new StringBuilder(); + for(var i = 0; i < options.Length.GetValueOrDefault(); i++) + { + var positionChars = string.Empty; + switch(positions[i]) + { + case 'l': + positionChars = lowercaseCharSet; + break; + case 'u': + positionChars = uppercaseCharSet; + break; + case 'n': + positionChars = numberCharSet; + break; + case 's': + positionChars = specialCharSet; + break; + case 'a': + positionChars = allCharSet; + break; + } + + var randomCharIndex = await _cryptoService.RandomNumberAsync(0, positionChars.Length - 1); + password.Append(positionChars[randomCharIndex]); + } + + return password.ToString(); + } + + public async Task GeneratePassphraseAsync(PasswordGenerationOptions options) + { + options.Merge(_defaultOptions); + if(options.NumWords.GetValueOrDefault() <= 2) + { + options.NumWords = _defaultOptions.NumWords; + } + if(options.WordSeparator == null || options.WordSeparator.Length == 0 || options.WordSeparator.Length > 1) + { + options.WordSeparator = " "; + } + var listLength = WordList.EEFLongWordList.Count - 1; + var wordList = new List(); + for(int i = 0; i < options.NumWords.GetValueOrDefault(); i++) + { + var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength); + wordList.Add(WordList.EEFLongWordList[wordIndex]); + } + return string.Join(options.WordSeparator, wordList); + } + + public async Task GetOptionsAsync() + { + if(_optionsCache == null) + { + var options = await _storageService.GetAsync(Keys_Options); + if(options == null) + { + _optionsCache = _defaultOptions; + } + else + { + options.Merge(_defaultOptions); + _optionsCache = options; + } + } + return _optionsCache; + } + + public async Task SaveOptionsAsync(PasswordGenerationOptions options) + { + await _storageService.SaveAsync(Keys_Options, options); + _optionsCache = options; + } + + public async Task> GetHistoryAsync() + { + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) + { + return new List(); + } + if(_history == null) + { + var encrypted = await _storageService.GetAsync>(Keys_History); + _history = await DecryptHistoryAsync(encrypted); + } + return _history ?? new List(); + } + + public async Task AddHistoryAsync(string password) + { + var hasKey = await _cryptoService.HasKeyAsync(); + if(!hasKey) + { + return; + } + var currentHistory = await GetHistoryAsync(); + // Prevent duplicates + if(MatchesPrevious(password, currentHistory)) + { + return; + } + currentHistory.Insert(0, new GeneratedPasswordHistory { Password = password, Date = DateTime.UtcNow }); + // Remove old items. + if(currentHistory.Count > MaxPasswordsInHistory) + { + currentHistory.RemoveAt(currentHistory.Count - 1); + } + var newHistory = await EncryptHistoryAsync(currentHistory); + await _storageService.SaveAsync(Keys_History, newHistory); + } + + public async Task ClearAsync() + { + _history = new List(); + await _storageService.RemoveAsync(Keys_History); + } + + public Task PasswordStrength(string password, List userInputs = null) + { + throw new NotImplementedException(); + } + + public void NormalizeOptions(PasswordGenerationOptions options) + { + options.MinLowercase = 0; + options.MinUppercase = 0; + + if(!options.Uppercase.GetValueOrDefault() && !options.Lowercase.GetValueOrDefault() && + !options.Number.GetValueOrDefault() && !options.Special.GetValueOrDefault()) + { + options.Lowercase = true; + } + + var length = options.Length.GetValueOrDefault(); + if(length < 5) + { + options.Length = 5; + } + else if(length > 128) + { + options.Length = 128; + } + + if(options.MinNumber == null) + { + options.MinNumber = 0; + } + else if(options.MinNumber > options.Length) + { + options.MinNumber = options.Length; + } + else if(options.MinNumber > 9) + { + options.MinNumber = 9; + } + + if(options.MinSpecial == null) + { + options.MinSpecial = 0; + } + else if(options.MinSpecial > options.Length) + { + options.MinSpecial = options.Length; + } + else if(options.MinSpecial > 9) + { + options.MinSpecial = 9; + } + + if(options.MinSpecial + options.MinNumber > options.Length) + { + options.MinSpecial = options.Length - options.MinNumber; + } + + if(options.NumWords == null || options.Length < 3) + { + options.NumWords = 3; + } + else if(options.NumWords > 20) + { + options.NumWords = 20; + } + + if(options.WordSeparator != null && options.WordSeparator.Length > 1) + { + options.WordSeparator = options.WordSeparator[0].ToString(); + } + } + + // Helpers + + private async Task> EncryptHistoryAsync(List history) + { + if(!history?.Any() ?? true) + { + return new List(); + } + var tasks = history.Select(async item => + { + var encrypted = await _cryptoService.EncryptAsync(item.Password); + return new GeneratedPasswordHistory + { + Password = encrypted.EncryptedString, + Date = item.Date + }; + }); + var h = await Task.WhenAll(tasks); + return h.ToList(); + } + + private async Task> DecryptHistoryAsync(List history) + { + if(!history?.Any() ?? true) + { + return new List(); + } + var tasks = history.Select(async item => + { + var decrypted = await _cryptoService.DecryptToUtf8Async(new CipherString(item.Password)); + return new GeneratedPasswordHistory + { + Password = decrypted, + Date = item.Date + }; + }); + var h = await Task.WhenAll(tasks); + return h.ToList(); + } + + private bool MatchesPrevious(string password, List history) + { + if(!history?.Any() ?? true) + { + return false; + } + return history.Last().Password == password; + } + } +} diff --git a/src/Core/Services/PclCryptoFunctionService.cs b/src/Core/Services/PclCryptoFunctionService.cs new file mode 100644 index 000000000..71312c1b4 --- /dev/null +++ b/src/Core/Services/PclCryptoFunctionService.cs @@ -0,0 +1,207 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using PCLCrypto; +using System; +using System.Text; +using System.Threading.Tasks; +using static PCLCrypto.WinRTCrypto; + +namespace Bit.Core.Services +{ + public class PclCryptoFunctionService : ICryptoFunctionService + { + private readonly ICryptoPrimitiveService _cryptoPrimitiveService; + + public PclCryptoFunctionService(ICryptoPrimitiveService cryptoPrimitiveService) + { + _cryptoPrimitiveService = cryptoPrimitiveService; + } + + public Task Pbkdf2Async(string password, string salt, CryptoHashAlgorithm algorithm, int iterations) + { + return Pbkdf2Async(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), algorithm, iterations); + } + + public Task Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations) + { + return Pbkdf2Async(password, Encoding.UTF8.GetBytes(salt), algorithm, iterations); + } + + public Task Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) + { + return Pbkdf2Async(Encoding.UTF8.GetBytes(password), salt, algorithm, iterations); + } + + public Task Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) + { + if(algorithm != CryptoHashAlgorithm.Sha256 && algorithm != CryptoHashAlgorithm.Sha512) + { + throw new ArgumentException("Unsupported PBKDF2 algorithm."); + } + return Task.FromResult(_cryptoPrimitiveService.Pbkdf2(password, salt, algorithm, iterations)); + } + + public Task HashAsync(string value, CryptoHashAlgorithm algorithm) + { + return HashAsync(Encoding.UTF8.GetBytes(value), algorithm); + } + + public Task HashAsync(byte[] value, CryptoHashAlgorithm algorithm) + { + var provider = HashAlgorithmProvider.OpenAlgorithm(ToHashAlgorithm(algorithm)); + return Task.FromResult(provider.HashData(value)); + } + + public Task HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm) + { + var provider = MacAlgorithmProvider.OpenAlgorithm(ToMacAlgorithm(algorithm)); + var hasher = provider.CreateHash(key); + hasher.Append(value); + return Task.FromResult(hasher.GetValueAndReset()); + } + + public async Task CompareAsync(byte[] a, byte[] b) + { + var provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256); + var hasher = provider.CreateHash(await RandomBytesAsync(32)); + + hasher.Append(a); + var mac1 = hasher.GetValueAndReset(); + hasher.Append(b); + var 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; + } + + public Task AesEncryptAsync(byte[] data, byte[] iv, byte[] key) + { + var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key); + return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data, iv)); + } + + public Task AesDecryptAsync(byte[] data, byte[] iv, byte[] key) + { + var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key); + return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data, iv)); + } + + public Task RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm) + { + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); + var cryptoKey = provider.ImportPublicKey(publicKey, + CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); + return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data)); + } + + public Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm) + { + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); + var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data)); + } + + public Task RsaExtractPublicKeyAsync(byte[] privateKey) + { + // Have to specify some algorithm + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); + var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo)); + } + + public Task> RsaGenerateKeyPairAsync(int length) + { + if(length != 1024 && length != 2048 && length != 4096) + { + throw new ArgumentException("Invalid key pair length."); + } + + // Have to specify some algorithm + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); + var cryptoKey = provider.CreateKeyPair(length); + var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); + var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult(new Tuple(publicKey, privateKey)); + } + + public Task RandomBytesAsync(int length) + { + return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); + } + + public byte[] RandomBytes(int length) + { + return CryptographicBuffer.GenerateRandom(length); + } + + public Task RandomNumberAsync() + { + return Task.FromResult(CryptographicBuffer.GenerateRandomNumber()); + } + + public uint RandomNumber() + { + return CryptographicBuffer.GenerateRandomNumber(); + } + + private HashAlgorithm ToHashAlgorithm(CryptoHashAlgorithm algorithm) + { + switch(algorithm) + { + case CryptoHashAlgorithm.Sha1: + return HashAlgorithm.Sha1; + case CryptoHashAlgorithm.Sha256: + return HashAlgorithm.Sha256; + case CryptoHashAlgorithm.Sha512: + return HashAlgorithm.Sha512; + case CryptoHashAlgorithm.Md5: + return HashAlgorithm.Md5; + default: + throw new ArgumentException("Unsupported hash algorithm."); + } + } + + private MacAlgorithm ToMacAlgorithm(CryptoHashAlgorithm algorithm) + { + switch(algorithm) + { + case CryptoHashAlgorithm.Sha1: + return MacAlgorithm.HmacSha1; + case CryptoHashAlgorithm.Sha256: + return MacAlgorithm.HmacSha256; + case CryptoHashAlgorithm.Sha512: + return MacAlgorithm.HmacSha512; + default: + throw new ArgumentException("Unsupported mac algorithm."); + } + } + + private AsymmetricAlgorithm ToAsymmetricAlgorithm(CryptoHashAlgorithm algorithm) + { + switch(algorithm) + { + case CryptoHashAlgorithm.Sha1: + return AsymmetricAlgorithm.RsaOaepSha1; + // RsaOaepSha256 is not supported on iOS + // ref: https://github.com/AArnott/PCLCrypto/issues/124 + // case CryptoHashAlgorithm.SHA256: + // return AsymmetricAlgorithm.RsaOaepSha256; + default: + throw new ArgumentException("Unsupported asymmetric algorithm."); + } + } + } +} diff --git a/src/Core/Services/SearchService.cs b/src/Core/Services/SearchService.cs new file mode 100644 index 000000000..e448f58c2 --- /dev/null +++ b/src/Core/Services/SearchService.cs @@ -0,0 +1,98 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class SearchService : ISearchService + { + private readonly ICipherService _cipherService; + + public SearchService( + ICipherService cipherService) + { + _cipherService = cipherService; + } + + public void ClearIndex() + { + // TODO + } + + public bool IsSearchable(string query) + { + return (query?.Length ?? 0) > 1; + } + + public Task IndexCiphersAsync() + { + // TODO + return Task.FromResult(0); + } + + public async Task> SearchCiphersAsync(string query, Func filter = null, + List ciphers = null, CancellationToken ct = default(CancellationToken)) + { + var results = new List(); + if(query != null) + { + query = query.Trim().ToLower(); + } + if(query == string.Empty) + { + query = null; + } + if(ciphers == null) + { + ciphers = await _cipherService.GetAllDecryptedAsync(); + } + + ct.ThrowIfCancellationRequested(); + if(filter != null) + { + ciphers = ciphers.Where(filter).ToList(); + } + + ct.ThrowIfCancellationRequested(); + if(!IsSearchable(query)) + { + return ciphers; + } + + return SearchCiphersBasic(ciphers, query); + // TODO: advanced searching with index + } + + public List SearchCiphersBasic(List ciphers, string query, + CancellationToken ct = default(CancellationToken)) + { + ct.ThrowIfCancellationRequested(); + query = query.Trim().ToLower(); + return ciphers.Where(c => + { + ct.ThrowIfCancellationRequested(); + if(c.Name?.ToLower().Contains(query) ?? false) + { + return true; + } + if(query.Length >= 8 && c.Id.StartsWith(query)) + { + return true; + } + if(c.SubTitle?.ToLower().Contains(query) ?? false) + { + return true; + } + if(c.Login?.Uri?.ToLower()?.Contains(query) ?? false) + { + return true; + } + return false; + }).ToList(); + } + } +} diff --git a/src/Core/Services/SettingsService.cs b/src/Core/Services/SettingsService.cs new file mode 100644 index 000000000..e8804f4a8 --- /dev/null +++ b/src/Core/Services/SettingsService.cs @@ -0,0 +1,89 @@ +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class SettingsService : ISettingsService + { + private const string Keys_SettingsFormat = "settings_{0}"; + private const string Keys_EquivalentDomains = "equivalentDomains"; + + private readonly IUserService _userService; + private readonly IStorageService _storageService; + + private Dictionary _settingsCache; + + public SettingsService( + IUserService userService, + IStorageService storageService) + { + _userService = userService; + _storageService = storageService; + } + + public void ClearCache() + { + _settingsCache?.Clear(); + _settingsCache = null; + } + + public async Task>> GetEquivalentDomainsAsync() + { + var settings = await GetSettingsAsync(); + if(settings != null && settings.ContainsKey(Keys_EquivalentDomains)) + { + var jArray = (settings[Keys_EquivalentDomains] as JArray); + return jArray.ToObject>>(); + } + return null; + } + + public Task SetEquivalentDomainsAsync(List> equivalentDomains) + { + return SetSettingsKeyAsync(Keys_EquivalentDomains, equivalentDomains); + } + + public async Task ClearAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_SettingsFormat, userId)); + ClearCache(); + } + + // Helpers + + private async Task> GetSettingsAsync() + { + if(_settingsCache == null) + { + var userId = await _userService.GetUserIdAsync(); + _settingsCache = await _storageService.GetAsync>( + string.Format(Keys_SettingsFormat, userId)); + } + return _settingsCache; + } + + private async Task SetSettingsKeyAsync(string key, T value) + { + var userId = await _userService.GetUserIdAsync(); + var settings = await GetSettingsAsync(); + if(settings == null) + { + settings = new Dictionary(); + } + if(settings.ContainsKey(key)) + { + settings[key] = value; + } + else + { + settings.Add(key, value); + } + await _storageService.SaveAsync(string.Format(Keys_SettingsFormat, userId), settings); + _settingsCache = settings; + } + } +} diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs new file mode 100644 index 000000000..c7096ec32 --- /dev/null +++ b/src/Core/Services/StateService.cs @@ -0,0 +1,44 @@ +using Bit.Core.Abstractions; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class StateService : IStateService + { + private readonly Dictionary _state = new Dictionary(); + + public Task GetAsync(string key) + { + return Task.FromResult(_state.ContainsKey(key) ? (T)_state[key] : (T)(object)null); + } + + public Task SaveAsync(string key, T obj) + { + if(_state.ContainsKey(key)) + { + _state[key] = obj; + } + else + { + _state.Add(key, obj); + } + return Task.FromResult(0); + } + + public Task RemoveAsync(string key) + { + if(_state.ContainsKey(key)) + { + _state.Remove(key); + } + return Task.FromResult(0); + } + + public Task PurgeAsync() + { + _state.Clear(); + return Task.FromResult(0); + } + } +} diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs new file mode 100644 index 000000000..764ddbb0f --- /dev/null +++ b/src/Core/Services/SyncService.cs @@ -0,0 +1,349 @@ +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class SyncService : ISyncService + { + private const string Keys_LastSyncFormat = "lastSync_{0}"; + + private readonly IUserService _userService; + private readonly IApiService _apiService; + private readonly ISettingsService _settingsService; + private readonly IFolderService _folderService; + private readonly ICipherService _cipherService; + private readonly ICryptoService _cryptoService; + private readonly ICollectionService _collectionService; + private readonly IStorageService _storageService; + private readonly IMessagingService _messagingService; + + public SyncService( + IUserService userService, + IApiService apiService, + ISettingsService settingsService, + IFolderService folderService, + ICipherService cipherService, + ICryptoService cryptoService, + ICollectionService collectionService, + IStorageService storageService, + IMessagingService messagingService) + { + _userService = userService; + _apiService = apiService; + _settingsService = settingsService; + _folderService = folderService; + _cipherService = cipherService; + _cryptoService = cryptoService; + _collectionService = collectionService; + _storageService = storageService; + _messagingService = messagingService; + } + + public bool SyncInProgress { get; set; } + + public async Task GetLastSyncAsync() + { + var userId = await _userService.GetUserIdAsync(); + if(userId == null) + { + return null; + } + return await _storageService.GetAsync(string.Format(Keys_LastSyncFormat, userId)); + } + + public async Task SetLastSyncAsync(DateTime date) + { + var userId = await _userService.GetUserIdAsync(); + if(userId == null) + { + return; + } + await _storageService.SaveAsync(string.Format(Keys_LastSyncFormat, userId), date); + } + + public async Task FullSyncAsync(bool forceSync) + { + SyncStarted(); + var isAuthenticated = await _userService.IsAuthenticatedAsync(); + if(!isAuthenticated) + { + return SyncCompleted(false); + } + var now = DateTime.UtcNow; + var needsSyncResult = await NeedsSyncingAsync(forceSync); + var needsSync = needsSyncResult.Item1; + var skipped = needsSyncResult.Item2; + if(skipped) + { + return SyncCompleted(false); + } + if(!needsSync) + { + await SetLastSyncAsync(now); + return SyncCompleted(false); + } + var userId = await _userService.GetUserIdAsync(); + try + { + var response = await _apiService.GetSyncAsync(); + await SyncProfileAsync(response.Profile); + await SyncFoldersAsync(userId, response.Folders); + await SyncCollectionsAsync(response.Collections); + await SyncCiphersAsync(userId, response.Ciphers); + await SyncSettingsAsync(userId, response.Domains); + await SetLastSyncAsync(now); + return SyncCompleted(true); + } + catch + { + return SyncCompleted(false); + } + } + + public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + try + { + var localFolder = await _folderService.GetAsync(notification.Id); + if((!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.RevisionDate < notification.RevisionDate)) + { + var remoteFolder = await _apiService.GetFolderAsync(notification.Id); + if(remoteFolder != null) + { + var userId = await _userService.GetUserIdAsync(); + await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); + _messagingService.Send("syncedUpsertedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch { } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + await _folderService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + + public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + try + { + var shouldUpdate = true; + var localCipher = await _cipherService.GetAsync(notification.Id); + if(localCipher != null && localCipher.RevisionDate >= notification.RevisionDate) + { + shouldUpdate = false; + } + + var checkCollections = false; + if(shouldUpdate) + { + if(isEdit) + { + shouldUpdate = localCipher != null; + checkCollections = true; + } + else + { + if(notification.CollectionIds == null || notification.OrganizationId == null) + { + shouldUpdate = localCipher == null; + } + else + { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if(!shouldUpdate && checkCollections && notification.OrganizationId != null && + notification.CollectionIds != null && notification.CollectionIds.Any()) + { + var collections = await _collectionService.GetAllAsync(); + if(collections != null) + { + foreach(var c in collections) + { + if(notification.CollectionIds.Contains(c.Id)) + { + shouldUpdate = true; + break; + } + } + } + } + + if(shouldUpdate) + { + var remoteCipher = await _apiService.GetCipherAsync(notification.Id); + if(remoteCipher != null) + { + var userId = await _userService.GetUserIdAsync(); + await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); + _messagingService.Send("syncedUpsertedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch(ApiException e) + { + if(e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound && isEdit) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + + // Helpers + + private void SyncStarted() + { + SyncInProgress = true; + _messagingService.Send("syncStarted"); + } + + private bool SyncCompleted(bool successfully) + { + SyncInProgress = false; + _messagingService.Send("syncCompleted", new Dictionary { ["successfully"] = successfully }); + return successfully; + } + + private async Task> NeedsSyncingAsync(bool forceSync) + { + if(forceSync) + { + return new Tuple(true, false); + } + var lastSync = await GetLastSyncAsync(); + if(lastSync == null || lastSync == DateTime.MinValue) + { + return new Tuple(true, false); + } + try + { + var response = await _apiService.GetAccountRevisionDateAsync(); + var d = CoreHelpers.Epoc.AddMilliseconds(response); + if(d <= lastSync.Value) + { + return new Tuple(false, false); + } + return new Tuple(true, false); + } + catch + { + return new Tuple(false, true); + } + } + + private async Task SyncProfileAsync(ProfileResponse response) + { + var stamp = await _userService.GetSecurityStampAsync(); + if(stamp != null && stamp != response.SecurityStamp) + { + // TODO logout callback + throw new Exception("Stamp has changed."); + } + await _cryptoService.SetEncKeyAsync(response.Key); + await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); + await _cryptoService.SetOrgKeysAsync(response.Organizations); + await _userService.SetSecurityStampAsync(response.SecurityStamp); + var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); + await _userService.ReplaceOrganizationsAsync(organizations); + } + + private async Task SyncFoldersAsync(string userId, List response) + { + var folders = response.ToDictionary(f => f.Id, f => new FolderData(f, userId)); + await _folderService.ReplaceAsync(folders); + } + + private async Task SyncCollectionsAsync(List response) + { + var collections = response.ToDictionary(c => c.Id, c => new CollectionData(c)); + await _collectionService.ReplaceAsync(collections); + } + + private async Task SyncCiphersAsync(string userId, List response) + { + var ciphers = response.ToDictionary(c => c.Id, c => new CipherData(c, userId)); + await _cipherService.ReplaceAsync(ciphers); + } + + private async Task SyncSettingsAsync(string userId, DomainsResponse response) + { + var eqDomains = new List>(); + if(response != null && response.EquivalentDomains != null) + { + eqDomains = eqDomains.Concat(response.EquivalentDomains).ToList(); + } + if(response != null && response.GlobalEquivalentDomains != null) + { + foreach(var global in response.GlobalEquivalentDomains) + { + if(global.Domains.Any()) + { + eqDomains.Add(global.Domains); + } + } + } + await _settingsService.SetEquivalentDomainsAsync(eqDomains); + } + } +} diff --git a/src/Core/Services/TokenService.cs b/src/Core/Services/TokenService.cs new file mode 100644 index 000000000..2597bb944 --- /dev/null +++ b/src/Core/Services/TokenService.cs @@ -0,0 +1,229 @@ +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Newtonsoft.Json.Linq; +using System; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class TokenService : ITokenService + { + private readonly IStorageService _storageService; + + private string _token; + private JObject _decodedToken; + private string _refreshToken; + + private const string Keys_AccessToken = "accessToken"; + private const string Keys_RefreshToken = "refreshToken"; + private const string Keys_TwoFactorTokenFormat = "twoFactorToken_{0}"; + + public TokenService(IStorageService storageService) + { + _storageService = storageService; + } + + public async Task SetTokensAsync(string accessToken, string refreshToken) + { + await Task.WhenAll( + SetTokenAsync(accessToken), + SetRefreshTokenAsync(refreshToken)); + } + + public async Task SetTokenAsync(string token) + { + _token = token; + _decodedToken = null; + await _storageService.SaveAsync(Keys_AccessToken, token); + } + + public async Task GetTokenAsync() + { + if(_token != null) + { + return _token; + } + _token = await _storageService.GetAsync(Keys_AccessToken); + return _token; + } + + public async Task SetRefreshTokenAsync(string refreshToken) + { + _refreshToken = refreshToken; + await _storageService.SaveAsync(Keys_RefreshToken, refreshToken); + } + + public async Task GetRefreshTokenAsync() + { + if(_refreshToken != null) + { + return _refreshToken; + } + _refreshToken = await _storageService.GetAsync(Keys_RefreshToken); + return _refreshToken; + } + + public async Task SetTwoFactorTokenAsync(string token, string email) + { + await _storageService.SaveAsync(string.Format(Keys_TwoFactorTokenFormat, email), token); + } + + public async Task GetTwoFactorTokenAsync(string email) + { + return await _storageService.GetAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + } + + public async Task ClearTwoFactorTokenAsync(string email) + { + await _storageService.RemoveAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + } + + public async Task ClearTokenAsync() + { + _token = null; + _decodedToken = null; + _refreshToken = null; + await Task.WhenAll( + _storageService.RemoveAsync(Keys_AccessToken), + _storageService.RemoveAsync(Keys_RefreshToken)); + } + + public JObject DecodeToken() + { + if(_decodedToken != null) + { + return _decodedToken; + } + if(_token == null) + { + throw new InvalidOperationException("Token not found."); + } + var parts = _token.Split('.'); + if(parts.Length != 3) + { + throw new InvalidOperationException("JWT must have 3 parts."); + } + var decodedBytes = Base64UrlDecode(parts[1]); + if(decodedBytes == null || decodedBytes.Length < 1) + { + throw new InvalidOperationException("Cannot decode the token."); + } + _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); + return _decodedToken; + } + + public DateTime? GetTokenExpirationDate() + { + var decoded = DecodeToken(); + if(decoded?["exp"] == null) + { + return null; + } + return CoreHelpers.Epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value())); + } + + public int TokenSecondsRemaining() + { + var d = GetTokenExpirationDate(); + if(d == null) + { + return 0; + } + var timeRemaining = d.Value - DateTime.UtcNow; + return (int)timeRemaining.TotalSeconds; + } + + public bool TokenNeedsRefresh(int minutes = 5) + { + var sRemaining = TokenSecondsRemaining(); + return sRemaining < (60 * minutes); + } + + public string GetUserId() + { + var decoded = DecodeToken(); + if(decoded?["sub"] == null) + { + throw new Exception("No user id found."); + } + return decoded["sub"].Value(); + } + + public string GetEmail() + { + var decoded = DecodeToken(); + if(decoded?["email"] == null) + { + throw new Exception("No email found."); + } + return decoded["email"].Value(); + } + + public bool GetEmailVerified() + { + var decoded = DecodeToken(); + if(decoded?["email_verified"] == null) + { + throw new Exception("No email verification found."); + } + return decoded["email_verified"].Value(); + } + + public string GetName() + { + var decoded = DecodeToken(); + if(decoded?["name"] == null) + { + return null; + } + return decoded["name"].Value(); + } + + public bool GetPremium() + { + var decoded = DecodeToken(); + if(decoded?["premium"] == null) + { + return false; + } + return decoded["premium"].Value(); + } + + public string GetIssuer() + { + var decoded = DecodeToken(); + if(decoded?["iss"] == null) + { + throw new Exception("No issuer found."); + } + return decoded["iss"].Value(); + } + + private byte[] Base64UrlDecode(string input) + { + var output = input; + // 62nd char of encoding + output = output.Replace('-', '+'); + // 63rd char of encoding + output = output.Replace('_', '/'); + // Pad with trailing '='s + switch(output.Length % 4) + { + case 0: + // No pad chars in this case + break; + case 2: + // Two pad chars + output += "=="; break; + case 3: + // One pad char + output += "="; break; + default: + throw new InvalidOperationException("Illegal base64url string!"); + } + // Standard base64 decoder + return Convert.FromBase64String(output); + } + } +} diff --git a/src/Core/Services/TotpService.cs b/src/Core/Services/TotpService.cs new file mode 100644 index 000000000..21c006539 --- /dev/null +++ b/src/Core/Services/TotpService.cs @@ -0,0 +1,142 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class TotpService : ITotpService + { + private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; + + private readonly IStorageService _storageService; + private readonly ICryptoFunctionService _cryptoFunctionService; + + public TotpService( + IStorageService storageService, + ICryptoFunctionService cryptoFunctionService) + { + _storageService = storageService; + _cryptoFunctionService = cryptoFunctionService; + } + + public async Task GetCodeAsync(string key) + { + if(string.IsNullOrWhiteSpace(key)) + { + return null; + } + var period = 30; + var alg = CryptoHashAlgorithm.Sha1; + var digits = 6; + var keyB32 = key; + + var isOtpAuth = key?.ToLowerInvariant().StartsWith("otpauth://") ?? false; + var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false; + if(isOtpAuth) + { + var qsParams = CoreHelpers.GetQueryParams(key); + if(qsParams.ContainsKey("digits") && qsParams["digits"] != null && + int.TryParse(qsParams["digits"].Trim(), out var digitParam)) + { + if(digitParam > 10) + { + digits = 10; + } + else if(digitParam > 0) + { + digits = digitParam; + } + } + if(qsParams.ContainsKey("period") && qsParams["period"] != null && + int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0) + { + period = periodParam; + } + if(qsParams.ContainsKey("secret") && qsParams["secret"] != null) + { + keyB32 = qsParams["secret"]; + } + if(qsParams.ContainsKey("algorithm") && qsParams["algorithm"] != null) + { + var algParam = qsParams["algorithm"].ToLowerInvariant(); + if(algParam == "sha256") + { + alg = CryptoHashAlgorithm.Sha256; + } + else if(algParam == "sha512") + { + alg = CryptoHashAlgorithm.Sha512; + } + } + } + else if(isSteamAuth) + { + digits = 5; + keyB32 = key.Substring(8); + } + + var keyBytes = Base32.FromBase32(keyB32); + if(keyBytes == null || keyBytes.Length == 0) + { + return null; + } + var now = CoreHelpers.EpocUtcNow() / 1000; + var time = now / period; + var timeBytes = BitConverter.GetBytes(time); + if(BitConverter.IsLittleEndian) + { + Array.Reverse(timeBytes, 0, timeBytes.Length); + } + + var hash = await _cryptoFunctionService.HmacAsync(timeBytes, keyBytes, alg); + if(hash.Length == 0) + { + return null; + } + + var offset = (hash[hash.Length - 1] & 0xf); + var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); + + string otp = string.Empty; + if(isSteamAuth) + { + var fullCode = binary & 0x7fffffff; + for(var i = 0; i < digits; i++) + { + otp += SteamChars[fullCode % SteamChars.Length]; + fullCode = (int)Math.Truncate(fullCode / (double)SteamChars.Length); + } + } + else + { + var rawOtp = binary % (int)Math.Pow(10, digits); + otp = rawOtp.ToString().PadLeft(digits, '0'); + } + return otp; + } + + public int GetTimeInterval(string key) + { + var period = 30; + if(key != null && key.ToLowerInvariant().StartsWith("otpauth://")) + { + var qsParams = CoreHelpers.GetQueryParams(key); + if(qsParams.ContainsKey("period") && qsParams["period"] != null && + int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0) + { + period = periodParam; + } + } + return period; + } + + public async Task IsAutoCopyEnabledAsync() + { + var disabled = await _storageService.GetAsync(Constants.DisableAutoTotpCopyKey); + return !disabled.GetValueOrDefault(); + } + } +} diff --git a/src/Core/Services/UserService.cs b/src/Core/Services/UserService.cs new file mode 100644 index 000000000..fc2597214 --- /dev/null +++ b/src/Core/Services/UserService.cs @@ -0,0 +1,167 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class UserService : IUserService + { + private string _userId; + private string _email; + private string _stamp; + private KdfType? _kdf; + private int? _kdfIterations; + + private const string Keys_UserId = "userId"; + private const string Keys_UserEmail = "userEmail"; + private const string Keys_Stamp = "securityStamp"; + private const string Keys_Kdf = "kdf"; + private const string Keys_KdfIterations = "kdfIterations"; + private const string Keys_OrganizationsFormat = "organizations_{0}"; + + private readonly IStorageService _storageService; + private readonly ITokenService _tokenService; + + public UserService(IStorageService storageService, ITokenService tokenService) + { + _storageService = storageService; + _tokenService = tokenService; + } + + public async Task SetInformationAsync(string userId, string email, KdfType kdf, int kdfIterations) + { + _email = email; + _userId = userId; + _kdf = kdf; + _kdfIterations = kdfIterations; + await Task.WhenAll( + _storageService.SaveAsync(Keys_UserEmail, email), + _storageService.SaveAsync(Keys_UserId, userId), + _storageService.SaveAsync(Keys_Kdf, (int)kdf), + _storageService.SaveAsync(Keys_KdfIterations, kdfIterations)); + } + + public async Task SetSecurityStampAsync(string stamp) + { + _stamp = stamp; + await _storageService.SaveAsync(Keys_Stamp, stamp); + } + + public async Task GetUserIdAsync() + { + if(_userId == null) + { + _userId = await _storageService.GetAsync(Keys_UserId); + } + return _userId; + } + + public async Task GetEmailAsync() + { + if(_email == null) + { + _email = await _storageService.GetAsync(Keys_UserEmail); + } + return _email; + } + + public async Task GetSecurityStampAsync() + { + if(_stamp == null) + { + _stamp = await _storageService.GetAsync(Keys_Stamp); + } + return _stamp; + } + + public async Task GetKdfAsync() + { + if(_kdf == null) + { + _kdf = (KdfType?)(await _storageService.GetAsync(Keys_Kdf)); + } + return _kdf; + } + + public async Task GetKdfIterationsAsync() + { + if(_kdfIterations == null) + { + _kdfIterations = await _storageService.GetAsync(Keys_KdfIterations); + } + return _kdfIterations; + } + + public async Task ClearAsync() + { + var userId = await GetUserIdAsync(); + await Task.WhenAll( + _storageService.RemoveAsync(Keys_UserId), + _storageService.RemoveAsync(Keys_UserEmail), + _storageService.RemoveAsync(Keys_Stamp), + _storageService.RemoveAsync(Keys_Kdf), + _storageService.RemoveAsync(Keys_KdfIterations), + ClearOrganizationsAsync(userId)); + _userId = _email = _stamp = null; + _kdf = null; + _kdfIterations = null; + } + + public async Task IsAuthenticatedAsync() + { + var token = await _tokenService.GetTokenAsync(); + if(token == null) + { + return false; + } + var userId = await GetUserIdAsync(); + return userId != null; + } + + public async Task CanAccessPremiumAsync() + { + var tokenPremium = _tokenService.GetPremium(); + if(tokenPremium) + { + return true; + } + var orgs = await GetAllOrganizationAsync(); + return orgs?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; + } + + public async Task GetOrganizationAsync(string id) + { + var userId = await GetUserIdAsync(); + var organizations = await _storageService.GetAsync>( + string.Format(Keys_OrganizationsFormat, userId)); + if(organizations == null || !organizations.ContainsKey(id)) + { + return null; + } + return new Organization(organizations[id]); + } + + public async Task> GetAllOrganizationAsync() + { + var userId = await GetUserIdAsync(); + var organizations = await _storageService.GetAsync>( + string.Format(Keys_OrganizationsFormat, userId)); + return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); + } + + public async Task ReplaceOrganizationsAsync(Dictionary organizations) + { + var userId = await GetUserIdAsync(); + await _storageService.SaveAsync(string.Format(Keys_OrganizationsFormat, userId), organizations); + } + + public async Task ClearOrganizationsAsync(string userId) + { + await _storageService.RemoveAsync(string.Format(Keys_OrganizationsFormat, userId)); + } + } +} diff --git a/src/App/Utilities/Base32.cs b/src/Core/Utilities/Base32.cs similarity index 98% rename from src/App/Utilities/Base32.cs rename to src/Core/Utilities/Base32.cs index 9d036360d..310f026f1 100644 --- a/src/App/Utilities/Base32.cs +++ b/src/Core/Utilities/Base32.cs @@ -1,6 +1,6 @@ using System; -namespace Bit.App.Utilities +namespace Bit.Core.Utilities { // ref: https://github.com/aspnet/Identity/blob/dev/src/Microsoft.Extensions.Identity.Core/Base32.cs // with some modifications for cleaning input diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs new file mode 100644 index 000000000..30770289c --- /dev/null +++ b/src/Core/Utilities/CoreHelpers.cs @@ -0,0 +1,183 @@ +using Bit.Core.Models.Domain; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Bit.Core.Utilities +{ + public static class CoreHelpers + { + public static readonly string IpRegex = + "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + public static readonly string TldEndingRegex = + ".*\\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$"; + + public static readonly DateTime Epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static long EpocUtcNow() + { + return (long)(DateTime.UtcNow - Epoc).TotalMilliseconds; + } + + public static bool InDebugMode() + { +#if DEBUG + return true; +#else + return false; +#endif + } + + public static string GetHostname(string uriString) + { + return GetUri(uriString)?.Host; + } + + public static string GetHost(string uriString) + { + var uri = GetUri(uriString); + if(uri != null) + { + if(uri.IsDefaultPort) + { + return uri.Host; + } + else + { + return string.Format("{0}:{1}", uri.Host, uri.Port); + } + } + return null; + } + + public static string GetDomain(string uriString) + { + var uri = GetUri(uriString); + if(uri == null) + { + return null; + } + + if(uri.Host == "localhost" || Regex.IsMatch(uriString, IpRegex)) + { + return uri.Host; + } + try + { + if(DomainName.TryParseBaseDomain(uri.Host, out var baseDomain)) + { + return baseDomain ?? uri.Host; + } + } + catch { } + return null; + } + + private static Uri GetUri(string uriString) + { + if(string.IsNullOrWhiteSpace(uriString)) + { + return null; + } + var httpUrl = uriString.StartsWith("https://") || uriString.StartsWith("http://"); + if(!httpUrl && !uriString.Contains("://") && Regex.IsMatch(uriString, TldEndingRegex)) + { + uriString = "http://" + uriString; + } + if(Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + return uri; + } + return null; + } + + public static void NestedTraverse(List> nodeTree, int partIndex, string[] parts, + T obj, T parent, char delimiter) where T : ITreeNodeObject + { + if(parts.Length <= partIndex) + { + return; + } + + var end = partIndex == parts.Length - 1; + var partName = parts[partIndex]; + foreach(var n in nodeTree) + { + if(n.Node.Name != parts[partIndex]) + { + continue; + } + if(end && n.Node.Id != obj.Id) + { + // Another node with the same name. + nodeTree.Add(new TreeNode(obj, partName, parent)); + return; + } + NestedTraverse(n.Children, partIndex + 1, parts, obj, n.Node, delimiter); + return; + } + if(!nodeTree.Any(n => n.Node.Name == partName)) + { + if(end) + { + nodeTree.Add(new TreeNode(obj, partName, parent)); + return; + } + var newPartName = string.Concat(parts[partIndex], delimiter, parts[partIndex + 1]); + var newParts = new List { newPartName }; + var newPartsStartFrom = partIndex + 2; + newParts.AddRange(new ArraySegment(parts, newPartsStartFrom, parts.Length - newPartsStartFrom)); + NestedTraverse(nodeTree, 0, newParts.ToArray(), obj, parent, delimiter); + } + } + + public static TreeNode GetTreeNodeObject(List> nodeTree, string id) where T : ITreeNodeObject + { + foreach(var n in nodeTree) + { + if(n.Node.Id == id) + { + return n; + } + else if(n.Children != null) + { + var node = GetTreeNodeObject(n.Children, id); + if(node != null) + { + return node; + } + } + } + return null; + } + + public static Dictionary GetQueryParams(string urlString) + { + var dict = new Dictionary(); + if(!Uri.TryCreate(urlString, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Query)) + { + return dict; + } + var pairs = uri.Query.Substring(1).Split('&'); + foreach(var pair in pairs) + { + var parts = pair.Split('='); + if(parts.Length < 1) + { + continue; + } + var key = System.Net.WebUtility.UrlDecode(parts[0]).ToLower(); + if(!dict.ContainsKey(key)) + { + dict.Add(key, parts[1] == null ? string.Empty : System.Net.WebUtility.UrlDecode(parts[1])); + } + } + return dict; + } + } +} diff --git a/src/App/Models/DomainName.cs b/src/Core/Utilities/DomainName.cs similarity index 84% rename from src/App/Models/DomainName.cs rename to src/Core/Utilities/DomainName.cs index 0c21f218d..955d6d3e8 100644 --- a/src/App/Models/DomainName.cs +++ b/src/Core/Utilities/DomainName.cs @@ -4,42 +4,35 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; -namespace Bit.App.Models +namespace Bit.Core.Utilities { // ref: https://github.com/danesparza/domainname-parser public class DomainName { - private const string IpRegex = "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + - "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + - "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + - "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; - - private string _subDomain = string.Empty; - private string _domain = string.Empty; - private string _tld = string.Empty; - private TLDRule _tldRule = null; + private readonly string _subDomain = string.Empty; + private readonly string _domain = string.Empty; + private readonly string _tld = string.Empty; + private readonly TLDRule _tldRule = null; public string SubDomain => _subDomain; public string Domain => _domain; - public string SLD => _domain; - public string TLD => _tld; + public string Sld => _domain; + public string Tld => _tld; public TLDRule Rule => _tldRule; public string BaseDomain => $"{_domain}.{_tld}"; - public DomainName(string TLD, string SLD, string SubDomain, TLDRule TLDRule) + public DomainName(string tld, string sld, string subDdomain, TLDRule tldRule) { - _tld = TLD; - _domain = SLD; - _subDomain = SubDomain; - _tldRule = TLDRule; + _tld = tld; + _domain = sld; + _subDomain = subDdomain; + _tldRule = tldRule; } public static bool TryParse(string domainString, out DomainName result) { bool retval = false; - // Our temporary domain parts: var tld = string.Empty; var sld = string.Empty; @@ -61,25 +54,18 @@ namespace Bit.App.Models // Looks like something bad happened -- return 'false' retval = false; } - return retval; } public static bool TryParseBaseDomain(string domainString, out string result) { - if(Regex.IsMatch(domainString, IpRegex)) - { - result = domainString; - return true; - } - - DomainName domain; - var retval = TryParse(domainString, out domain); + var retVal = TryParse(domainString, out DomainName domain); result = domain?.BaseDomain; - return retval; + return retVal; } - private static void ParseDomainName(string domainString, out string TLD, out string SLD, out string SubDomain, out TLDRule MatchingRule) + private static void ParseDomainName(string domainString, out string TLD, out string SLD, + out string SubDomain, out TLDRule MatchingRule) { // Make sure domain is all lowercase domainString = domainString.ToLower(); @@ -157,7 +143,8 @@ namespace Bit.App.Models { // Split our domain into parts (based on the '.') // ...Put these parts in a list - // ...Make sure these parts are in reverse order (we'll be checking rules from the right-most pat of the domain) + // ...Make sure these parts are in reverse order (we'll be checking rules from the + // right -most pat of the domain) var lstDomainParts = domainString.Split('.').ToList(); lstDomainParts.Reverse(); @@ -182,12 +169,12 @@ namespace Bit.App.Models foreach(var rule in rules) { // Try to match rule: - TLDRule result; - if(TLDRulesCache.Instance.TLDRuleLists[rule].TryGetValue(checkAgainst, out result)) + if(TLDRulesCache.Instance.TLDRuleLists[rule].TryGetValue(checkAgainst, out TLDRule result)) { ruleMatches.Add(result); } - //Debug.WriteLine(string.Format("Domain part {0} matched {1} {2} rules", checkAgainst, result == null ? 0 : 1, rule)); + //Debug.WriteLine(string.Format("Domain part {0} matched {1} {2} rules", + // checkAgainst, result == null ? 0 : 1, rule)); } } @@ -200,7 +187,8 @@ namespace Bit.App.Models TLDRule primaryMatch = results.Take(1).SingleOrDefault(); if(primaryMatch != null) { - //Debug.WriteLine(string.Format("Looks like our match is: {0}, which is a(n) {1} rule.", primaryMatch.Name, primaryMatch.Type)); + //Debug.WriteLine(string.Format("Looks like our match is: {0}, which is a(n) {1} rule.", + // primaryMatch.Name, primaryMatch.Type)); } else { @@ -215,23 +203,23 @@ namespace Bit.App.Models public string Name { get; private set; } public RuleType Type { get; private set; } - public TLDRule(string RuleInfo) + public TLDRule(string ruleInfo) { // Parse the rule and set properties accordingly: - if(RuleInfo.StartsWith("*")) + if(ruleInfo.StartsWith("*")) { Type = RuleType.Wildcard; - Name = RuleInfo.Substring(2); + Name = ruleInfo.Substring(2); } - else if(RuleInfo.StartsWith("!")) + else if(ruleInfo.StartsWith("!")) { Type = RuleType.Exception; - Name = RuleInfo.Substring(1); + Name = ruleInfo.Substring(1); } else { Type = RuleType.Normal; - Name = RuleInfo; + Name = ruleInfo; } } @@ -241,7 +229,6 @@ namespace Bit.App.Models { return -1; } - return Name.CompareTo(other.Name); } @@ -253,7 +240,7 @@ namespace Bit.App.Models } } - public class TLDRulesCache + private class TLDRulesCache { private static volatile TLDRulesCache _uniqueInstance; private static object _syncObj = new object(); @@ -315,25 +302,30 @@ namespace Bit.App.Models } var ruleStrings = ReadRulesData(); - // Strip out any lines that are: // a.) A comment // b.) Blank - foreach(var ruleString in ruleStrings.Where(ruleString => !ruleString.StartsWith("//") && ruleString.Trim().Length != 0)) + var filteredRuleString = ruleStrings.Where(ruleString => + !ruleString.StartsWith("//") && ruleString.Trim().Length != 0); + foreach(var ruleString in filteredRuleString) { var result = new TLDRule(ruleString); results[result.Type][result.Name] = result; } // Return our results: - Debug.WriteLine(string.Format("Loaded {0} rules into cache.", results.Values.Sum(r => r.Values.Count))); + if(CoreHelpers.InDebugMode()) + { + Debug.WriteLine(string.Format("Loaded {0} rules into cache.", + results.Values.Sum(r => r.Values.Count))); + } return results; } private IEnumerable ReadRulesData() { var assembly = typeof(TLDRulesCache).GetTypeInfo().Assembly; - var stream = assembly.GetManifestResourceStream("Bit.App.Resources.public_suffix_list.dat"); + var stream = assembly.GetManifestResourceStream("Bit.Core.Resources.public_suffix_list.dat"); string line; using(var reader = new StreamReader(stream)) { diff --git a/src/App/Utilities/ExtendedObservableCollection.cs b/src/Core/Utilities/ExtendedObservableCollection.cs similarity index 75% rename from src/App/Utilities/ExtendedObservableCollection.cs rename to src/Core/Utilities/ExtendedObservableCollection.cs index 4a5c0fa5d..473e3c7b9 100644 --- a/src/App/Utilities/ExtendedObservableCollection.cs +++ b/src/Core/Utilities/ExtendedObservableCollection.cs @@ -1,16 +1,20 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; -namespace Bit.App.Utilities +namespace Bit.Core.Utilities { public class ExtendedObservableCollection : ObservableCollection { - public ExtendedObservableCollection() : base() { } - public ExtendedObservableCollection(IEnumerable collection) : base(collection) { } - public ExtendedObservableCollection(List list) : base(list) { } + public ExtendedObservableCollection() + : base() { } + + public ExtendedObservableCollection(IEnumerable collection) + : base(collection) { } + + public ExtendedObservableCollection(List list) + : base(list) { } public void AddRange(IEnumerable range) { diff --git a/src/Core/Utilities/ExtendedViewModel.cs b/src/Core/Utilities/ExtendedViewModel.cs new file mode 100644 index 000000000..123d502da --- /dev/null +++ b/src/Core/Utilities/ExtendedViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Bit.Core.Utilities +{ + public abstract class ExtendedViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected bool SetProperty(ref T backingStore, T value, Action onChanged = null, + [CallerMemberName]string propertyName = "", string[] additionalPropertyNames = null) + { + if(EqualityComparer.Default.Equals(backingStore, value)) + { + return false; + } + + backingStore = value; + TriggerPropertyChanged(propertyName, additionalPropertyNames); + onChanged?.Invoke(); + return true; + } + + protected void TriggerPropertyChanged(string propertyName, string[] additionalPropertyNames = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + if(PropertyChanged != null && additionalPropertyNames != null) + { + foreach(var prop in additionalPropertyNames) + { + PropertyChanged.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + } + } +} diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs new file mode 100644 index 000000000..a7602df38 --- /dev/null +++ b/src/Core/Utilities/ServiceContainer.cs @@ -0,0 +1,101 @@ +using Bit.Core.Abstractions; +using Bit.Core.Services; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.Core.Utilities +{ + public static class ServiceContainer + { + public static Dictionary RegisteredServices { get; set; } = new Dictionary(); + public static bool Inited { get; set; } + + public static void Init() + { + if(Inited) + { + return; + } + Inited = true; + + var platformUtilsService = Resolve("platformUtilsService"); + var storageService = Resolve("storageService"); + var secureStorageService = Resolve("secureStorageService"); + var cryptoPrimitiveService = Resolve("cryptoPrimitiveService"); + var i18nService = Resolve("i18nService"); + var messagingService = Resolve("messagingService"); + SearchService searchService = null; + + var stateService = new StateService(); + var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); + var cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService); + var tokenService = new TokenService(storageService); + var apiService = new ApiService(tokenService, platformUtilsService, (bool expired) => Task.FromResult(0)); + var appIdService = new AppIdService(storageService); + var userService = new UserService(storageService, tokenService); + var settingsService = new SettingsService(userService, storageService); + var cipherService = new CipherService(cryptoService, userService, settingsService, apiService, + storageService, i18nService, () => searchService); + var folderService = new FolderService(cryptoService, userService, apiService, storageService, + i18nService, cipherService); + var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService); + searchService = new SearchService(cipherService); + var lockService = new LockService(cryptoService, userService, platformUtilsService, storageService, + folderService, cipherService, collectionService, searchService, messagingService); + var syncService = new SyncService(userService, apiService, settingsService, folderService, + cipherService, cryptoService, collectionService, storageService, messagingService); + var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, + cryptoFunctionService); + var totpService = new TotpService(storageService, cryptoFunctionService); + var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, + i18nService, platformUtilsService, messagingService); + // TODO: export service + var auditService = new AuditService(cryptoFunctionService, apiService); + var environmentService = new EnvironmentService(apiService, storageService); + // TODO: notification service + + Register("stateService", stateService); + Register("cryptoFunctionService", cryptoFunctionService); + Register("cryptoService", cryptoService); + Register("tokenService", tokenService); + Register("apiService", apiService); + Register("appIdService", appIdService); + Register("userService", userService); + Register("settingsService", settingsService); + Register("cipherService", cipherService); + Register("folderService", folderService); + Register("collectionService", collectionService); + Register("searchService", searchService); + Register("syncService", syncService); + Register("lockService", lockService); + Register("passwordGenerationService", passwordGenerationService); + Register("totpService", totpService); + Register("authService", authService); + Register("auditService", auditService); + Register("environmentService", environmentService); + } + + public static void Register(string serviceName, T obj) + { + if(RegisteredServices.ContainsKey(serviceName)) + { + throw new Exception($"Service {serviceName} has already been registered."); + } + RegisteredServices.Add(serviceName, obj); + } + + public static T Resolve(string serviceName, bool dontThrow = false) + { + if(RegisteredServices.ContainsKey(serviceName)) + { + return (T)RegisteredServices[serviceName]; + } + if(dontThrow) + { + return (T)(object)null; + } + throw new Exception($"Service {serviceName} is not registered."); + } + } +} diff --git a/src/Core/Utilities/WordList.cs b/src/Core/Utilities/WordList.cs new file mode 100644 index 000000000..dff9f8a3c --- /dev/null +++ b/src/Core/Utilities/WordList.cs @@ -0,0 +1,7788 @@ +using System.Collections.Generic; + +namespace Bit.Core.Utilities +{ + public static class WordList + { + // EFF"s Long Wordlist from https://www.eff.org/dice + public static readonly List EEFLongWordList = new List + { + "abacus", + "abdomen", + "abdominal", + "abide", + "abiding", + "ability", + "ablaze", + "able", + "abnormal", + "abrasion", + "abrasive", + "abreast", + "abridge", + "abroad", + "abruptly", + "absence", + "absentee", + "absently", + "absinthe", + "absolute", + "absolve", + "abstain", + "abstract", + "absurd", + "accent", + "acclaim", + "acclimate", + "accompany", + "account", + "accuracy", + "accurate", + "accustom", + "acetone", + "achiness", + "aching", + "acid", + "acorn", + "acquaint", + "acquire", + "acre", + "acrobat", + "acronym", + "acting", + "action", + "activate", + "activator", + "active", + "activism", + "activist", + "activity", + "actress", + "acts", + "acutely", + "acuteness", + "aeration", + "aerobics", + "aerosol", + "aerospace", + "afar", + "affair", + "affected", + "affecting", + "affection", + "affidavit", + "affiliate", + "affirm", + "affix", + "afflicted", + "affluent", + "afford", + "affront", + "aflame", + "afloat", + "aflutter", + "afoot", + "afraid", + "afterglow", + "afterlife", + "aftermath", + "aftermost", + "afternoon", + "aged", + "ageless", + "agency", + "agenda", + "agent", + "aggregate", + "aghast", + "agile", + "agility", + "aging", + "agnostic", + "agonize", + "agonizing", + "agony", + "agreeable", + "agreeably", + "agreed", + "agreeing", + "agreement", + "aground", + "ahead", + "ahoy", + "aide", + "aids", + "aim", + "ajar", + "alabaster", + "alarm", + "albatross", + "album", + "alfalfa", + "algebra", + "algorithm", + "alias", + "alibi", + "alienable", + "alienate", + "aliens", + "alike", + "alive", + "alkaline", + "alkalize", + "almanac", + "almighty", + "almost", + "aloe", + "aloft", + "aloha", + "alone", + "alongside", + "aloof", + "alphabet", + "alright", + "although", + "altitude", + "alto", + "aluminum", + "alumni", + "always", + "amaretto", + "amaze", + "amazingly", + "amber", + "ambiance", + "ambiguity", + "ambiguous", + "ambition", + "ambitious", + "ambulance", + "ambush", + "amendable", + "amendment", + "amends", + "amenity", + "amiable", + "amicably", + "amid", + "amigo", + "amino", + "amiss", + "ammonia", + "ammonium", + "amnesty", + "amniotic", + "among", + "amount", + "amperage", + "ample", + "amplifier", + "amplify", + "amply", + "amuck", + "amulet", + "amusable", + "amused", + "amusement", + "amuser", + "amusing", + "anaconda", + "anaerobic", + "anagram", + "anatomist", + "anatomy", + "anchor", + "anchovy", + "ancient", + "android", + "anemia", + "anemic", + "aneurism", + "anew", + "angelfish", + "angelic", + "anger", + "angled", + "angler", + "angles", + "angling", + "angrily", + "angriness", + "anguished", + "angular", + "animal", + "animate", + "animating", + "animation", + "animator", + "anime", + "animosity", + "ankle", + "annex", + "annotate", + "announcer", + "annoying", + "annually", + "annuity", + "anointer", + "another", + "answering", + "antacid", + "antarctic", + "anteater", + "antelope", + "antennae", + "anthem", + "anthill", + "anthology", + "antibody", + "antics", + "antidote", + "antihero", + "antiquely", + "antiques", + "antiquity", + "antirust", + "antitoxic", + "antitrust", + "antiviral", + "antivirus", + "antler", + "antonym", + "antsy", + "anvil", + "anybody", + "anyhow", + "anymore", + "anyone", + "anyplace", + "anything", + "anytime", + "anyway", + "anywhere", + "aorta", + "apache", + "apostle", + "appealing", + "appear", + "appease", + "appeasing", + "appendage", + "appendix", + "appetite", + "appetizer", + "applaud", + "applause", + "apple", + "appliance", + "applicant", + "applied", + "apply", + "appointee", + "appraisal", + "appraiser", + "apprehend", + "approach", + "approval", + "approve", + "apricot", + "april", + "apron", + "aptitude", + "aptly", + "aqua", + "aqueduct", + "arbitrary", + "arbitrate", + "ardently", + "area", + "arena", + "arguable", + "arguably", + "argue", + "arise", + "armadillo", + "armband", + "armchair", + "armed", + "armful", + "armhole", + "arming", + "armless", + "armoire", + "armored", + "armory", + "armrest", + "army", + "aroma", + "arose", + "around", + "arousal", + "arrange", + "array", + "arrest", + "arrival", + "arrive", + "arrogance", + "arrogant", + "arson", + "art", + "ascend", + "ascension", + "ascent", + "ascertain", + "ashamed", + "ashen", + "ashes", + "ashy", + "aside", + "askew", + "asleep", + "asparagus", + "aspect", + "aspirate", + "aspire", + "aspirin", + "astonish", + "astound", + "astride", + "astrology", + "astronaut", + "astronomy", + "astute", + "atlantic", + "atlas", + "atom", + "atonable", + "atop", + "atrium", + "atrocious", + "atrophy", + "attach", + "attain", + "attempt", + "attendant", + "attendee", + "attention", + "attentive", + "attest", + "attic", + "attire", + "attitude", + "attractor", + "attribute", + "atypical", + "auction", + "audacious", + "audacity", + "audible", + "audibly", + "audience", + "audio", + "audition", + "augmented", + "august", + "authentic", + "author", + "autism", + "autistic", + "autograph", + "automaker", + "automated", + "automatic", + "autopilot", + "available", + "avalanche", + "avatar", + "avenge", + "avenging", + "avenue", + "average", + "aversion", + "avert", + "aviation", + "aviator", + "avid", + "avoid", + "await", + "awaken", + "award", + "aware", + "awhile", + "awkward", + "awning", + "awoke", + "awry", + "axis", + "babble", + "babbling", + "babied", + "baboon", + "backache", + "backboard", + "backboned", + "backdrop", + "backed", + "backer", + "backfield", + "backfire", + "backhand", + "backing", + "backlands", + "backlash", + "backless", + "backlight", + "backlit", + "backlog", + "backpack", + "backpedal", + "backrest", + "backroom", + "backshift", + "backside", + "backslid", + "backspace", + "backspin", + "backstab", + "backstage", + "backtalk", + "backtrack", + "backup", + "backward", + "backwash", + "backwater", + "backyard", + "bacon", + "bacteria", + "bacterium", + "badass", + "badge", + "badland", + "badly", + "badness", + "baffle", + "baffling", + "bagel", + "bagful", + "baggage", + "bagged", + "baggie", + "bagginess", + "bagging", + "baggy", + "bagpipe", + "baguette", + "baked", + "bakery", + "bakeshop", + "baking", + "balance", + "balancing", + "balcony", + "balmy", + "balsamic", + "bamboo", + "banana", + "banish", + "banister", + "banjo", + "bankable", + "bankbook", + "banked", + "banker", + "banking", + "banknote", + "bankroll", + "banner", + "bannister", + "banshee", + "banter", + "barbecue", + "barbed", + "barbell", + "barber", + "barcode", + "barge", + "bargraph", + "barista", + "baritone", + "barley", + "barmaid", + "barman", + "barn", + "barometer", + "barrack", + "barracuda", + "barrel", + "barrette", + "barricade", + "barrier", + "barstool", + "bartender", + "barterer", + "bash", + "basically", + "basics", + "basil", + "basin", + "basis", + "basket", + "batboy", + "batch", + "bath", + "baton", + "bats", + "battalion", + "battered", + "battering", + "battery", + "batting", + "battle", + "bauble", + "bazooka", + "blabber", + "bladder", + "blade", + "blah", + "blame", + "blaming", + "blanching", + "blandness", + "blank", + "blaspheme", + "blasphemy", + "blast", + "blatancy", + "blatantly", + "blazer", + "blazing", + "bleach", + "bleak", + "bleep", + "blemish", + "blend", + "bless", + "blighted", + "blimp", + "bling", + "blinked", + "blinker", + "blinking", + "blinks", + "blip", + "blissful", + "blitz", + "blizzard", + "bloated", + "bloating", + "blob", + "blog", + "bloomers", + "blooming", + "blooper", + "blot", + "blouse", + "blubber", + "bluff", + "bluish", + "blunderer", + "blunt", + "blurb", + "blurred", + "blurry", + "blurt", + "blush", + "blustery", + "boaster", + "boastful", + "boasting", + "boat", + "bobbed", + "bobbing", + "bobble", + "bobcat", + "bobsled", + "bobtail", + "bodacious", + "body", + "bogged", + "boggle", + "bogus", + "boil", + "bok", + "bolster", + "bolt", + "bonanza", + "bonded", + "bonding", + "bondless", + "boned", + "bonehead", + "boneless", + "bonelike", + "boney", + "bonfire", + "bonnet", + "bonsai", + "bonus", + "bony", + "boogeyman", + "boogieman", + "book", + "boondocks", + "booted", + "booth", + "bootie", + "booting", + "bootlace", + "bootleg", + "boots", + "boozy", + "borax", + "boring", + "borough", + "borrower", + "borrowing", + "boss", + "botanical", + "botanist", + "botany", + "botch", + "both", + "bottle", + "bottling", + "bottom", + "bounce", + "bouncing", + "bouncy", + "bounding", + "boundless", + "bountiful", + "bovine", + "boxcar", + "boxer", + "boxing", + "boxlike", + "boxy", + "breach", + "breath", + "breeches", + "breeching", + "breeder", + "breeding", + "breeze", + "breezy", + "brethren", + "brewery", + "brewing", + "briar", + "bribe", + "brick", + "bride", + "bridged", + "brigade", + "bright", + "brilliant", + "brim", + "bring", + "brink", + "brisket", + "briskly", + "briskness", + "bristle", + "brittle", + "broadband", + "broadcast", + "broaden", + "broadly", + "broadness", + "broadside", + "broadways", + "broiler", + "broiling", + "broken", + "broker", + "bronchial", + "bronco", + "bronze", + "bronzing", + "brook", + "broom", + "brought", + "browbeat", + "brownnose", + "browse", + "browsing", + "bruising", + "brunch", + "brunette", + "brunt", + "brush", + "brussels", + "brute", + "brutishly", + "bubble", + "bubbling", + "bubbly", + "buccaneer", + "bucked", + "bucket", + "buckle", + "buckshot", + "buckskin", + "bucktooth", + "buckwheat", + "buddhism", + "buddhist", + "budding", + "buddy", + "budget", + "buffalo", + "buffed", + "buffer", + "buffing", + "buffoon", + "buggy", + "bulb", + "bulge", + "bulginess", + "bulgur", + "bulk", + "bulldog", + "bulldozer", + "bullfight", + "bullfrog", + "bullhorn", + "bullion", + "bullish", + "bullpen", + "bullring", + "bullseye", + "bullwhip", + "bully", + "bunch", + "bundle", + "bungee", + "bunion", + "bunkbed", + "bunkhouse", + "bunkmate", + "bunny", + "bunt", + "busboy", + "bush", + "busily", + "busload", + "bust", + "busybody", + "buzz", + "cabana", + "cabbage", + "cabbie", + "cabdriver", + "cable", + "caboose", + "cache", + "cackle", + "cacti", + "cactus", + "caddie", + "caddy", + "cadet", + "cadillac", + "cadmium", + "cage", + "cahoots", + "cake", + "calamari", + "calamity", + "calcium", + "calculate", + "calculus", + "caliber", + "calibrate", + "calm", + "caloric", + "calorie", + "calzone", + "camcorder", + "cameo", + "camera", + "camisole", + "camper", + "campfire", + "camping", + "campsite", + "campus", + "canal", + "canary", + "cancel", + "candied", + "candle", + "candy", + "cane", + "canine", + "canister", + "cannabis", + "canned", + "canning", + "cannon", + "cannot", + "canola", + "canon", + "canopener", + "canopy", + "canteen", + "canyon", + "capable", + "capably", + "capacity", + "cape", + "capillary", + "capital", + "capitol", + "capped", + "capricorn", + "capsize", + "capsule", + "caption", + "captivate", + "captive", + "captivity", + "capture", + "caramel", + "carat", + "caravan", + "carbon", + "cardboard", + "carded", + "cardiac", + "cardigan", + "cardinal", + "cardstock", + "carefully", + "caregiver", + "careless", + "caress", + "caretaker", + "cargo", + "caring", + "carless", + "carload", + "carmaker", + "carnage", + "carnation", + "carnival", + "carnivore", + "carol", + "carpenter", + "carpentry", + "carpool", + "carport", + "carried", + "carrot", + "carrousel", + "carry", + "cartel", + "cartload", + "carton", + "cartoon", + "cartridge", + "cartwheel", + "carve", + "carving", + "carwash", + "cascade", + "case", + "cash", + "casing", + "casino", + "casket", + "cassette", + "casually", + "casualty", + "catacomb", + "catalog", + "catalyst", + "catalyze", + "catapult", + "cataract", + "catatonic", + "catcall", + "catchable", + "catcher", + "catching", + "catchy", + "caterer", + "catering", + "catfight", + "catfish", + "cathedral", + "cathouse", + "catlike", + "catnap", + "catnip", + "catsup", + "cattail", + "cattishly", + "cattle", + "catty", + "catwalk", + "caucasian", + "caucus", + "causal", + "causation", + "cause", + "causing", + "cauterize", + "caution", + "cautious", + "cavalier", + "cavalry", + "caviar", + "cavity", + "cedar", + "celery", + "celestial", + "celibacy", + "celibate", + "celtic", + "cement", + "census", + "ceramics", + "ceremony", + "certainly", + "certainty", + "certified", + "certify", + "cesarean", + "cesspool", + "chafe", + "chaffing", + "chain", + "chair", + "chalice", + "challenge", + "chamber", + "chamomile", + "champion", + "chance", + "change", + "channel", + "chant", + "chaos", + "chaperone", + "chaplain", + "chapped", + "chaps", + "chapter", + "character", + "charbroil", + "charcoal", + "charger", + "charging", + "chariot", + "charity", + "charm", + "charred", + "charter", + "charting", + "chase", + "chasing", + "chaste", + "chastise", + "chastity", + "chatroom", + "chatter", + "chatting", + "chatty", + "cheating", + "cheddar", + "cheek", + "cheer", + "cheese", + "cheesy", + "chef", + "chemicals", + "chemist", + "chemo", + "cherisher", + "cherub", + "chess", + "chest", + "chevron", + "chevy", + "chewable", + "chewer", + "chewing", + "chewy", + "chief", + "chihuahua", + "childcare", + "childhood", + "childish", + "childless", + "childlike", + "chili", + "chill", + "chimp", + "chip", + "chirping", + "chirpy", + "chitchat", + "chivalry", + "chive", + "chloride", + "chlorine", + "choice", + "chokehold", + "choking", + "chomp", + "chooser", + "choosing", + "choosy", + "chop", + "chosen", + "chowder", + "chowtime", + "chrome", + "chubby", + "chuck", + "chug", + "chummy", + "chump", + "chunk", + "churn", + "chute", + "cider", + "cilantro", + "cinch", + "cinema", + "cinnamon", + "circle", + "circling", + "circular", + "circulate", + "circus", + "citable", + "citadel", + "citation", + "citizen", + "citric", + "citrus", + "city", + "civic", + "civil", + "clad", + "claim", + "clambake", + "clammy", + "clamor", + "clamp", + "clamshell", + "clang", + "clanking", + "clapped", + "clapper", + "clapping", + "clarify", + "clarinet", + "clarity", + "clash", + "clasp", + "class", + "clatter", + "clause", + "clavicle", + "claw", + "clay", + "clean", + "clear", + "cleat", + "cleaver", + "cleft", + "clench", + "clergyman", + "clerical", + "clerk", + "clever", + "clicker", + "client", + "climate", + "climatic", + "cling", + "clinic", + "clinking", + "clip", + "clique", + "cloak", + "clobber", + "clock", + "clone", + "cloning", + "closable", + "closure", + "clothes", + "clothing", + "cloud", + "clover", + "clubbed", + "clubbing", + "clubhouse", + "clump", + "clumsily", + "clumsy", + "clunky", + "clustered", + "clutch", + "clutter", + "coach", + "coagulant", + "coastal", + "coaster", + "coasting", + "coastland", + "coastline", + "coat", + "coauthor", + "cobalt", + "cobbler", + "cobweb", + "cocoa", + "coconut", + "cod", + "coeditor", + "coerce", + "coexist", + "coffee", + "cofounder", + "cognition", + "cognitive", + "cogwheel", + "coherence", + "coherent", + "cohesive", + "coil", + "coke", + "cola", + "cold", + "coleslaw", + "coliseum", + "collage", + "collapse", + "collar", + "collected", + "collector", + "collide", + "collie", + "collision", + "colonial", + "colonist", + "colonize", + "colony", + "colossal", + "colt", + "coma", + "come", + "comfort", + "comfy", + "comic", + "coming", + "comma", + "commence", + "commend", + "comment", + "commerce", + "commode", + "commodity", + "commodore", + "common", + "commotion", + "commute", + "commuting", + "compacted", + "compacter", + "compactly", + "compactor", + "companion", + "company", + "compare", + "compel", + "compile", + "comply", + "component", + "composed", + "composer", + "composite", + "compost", + "composure", + "compound", + "compress", + "comprised", + "computer", + "computing", + "comrade", + "concave", + "conceal", + "conceded", + "concept", + "concerned", + "concert", + "conch", + "concierge", + "concise", + "conclude", + "concrete", + "concur", + "condense", + "condiment", + "condition", + "condone", + "conducive", + "conductor", + "conduit", + "cone", + "confess", + "confetti", + "confidant", + "confident", + "confider", + "confiding", + "configure", + "confined", + "confining", + "confirm", + "conflict", + "conform", + "confound", + "confront", + "confused", + "confusing", + "confusion", + "congenial", + "congested", + "congrats", + "congress", + "conical", + "conjoined", + "conjure", + "conjuror", + "connected", + "connector", + "consensus", + "consent", + "console", + "consoling", + "consonant", + "constable", + "constant", + "constrain", + "constrict", + "construct", + "consult", + "consumer", + "consuming", + "contact", + "container", + "contempt", + "contend", + "contented", + "contently", + "contents", + "contest", + "context", + "contort", + "contour", + "contrite", + "control", + "contusion", + "convene", + "convent", + "copartner", + "cope", + "copied", + "copier", + "copilot", + "coping", + "copious", + "copper", + "copy", + "coral", + "cork", + "cornball", + "cornbread", + "corncob", + "cornea", + "corned", + "corner", + "cornfield", + "cornflake", + "cornhusk", + "cornmeal", + "cornstalk", + "corny", + "coronary", + "coroner", + "corporal", + "corporate", + "corral", + "correct", + "corridor", + "corrode", + "corroding", + "corrosive", + "corsage", + "corset", + "cortex", + "cosigner", + "cosmetics", + "cosmic", + "cosmos", + "cosponsor", + "cost", + "cottage", + "cotton", + "couch", + "cough", + "could", + "countable", + "countdown", + "counting", + "countless", + "country", + "county", + "courier", + "covenant", + "cover", + "coveted", + "coveting", + "coyness", + "cozily", + "coziness", + "cozy", + "crabbing", + "crabgrass", + "crablike", + "crabmeat", + "cradle", + "cradling", + "crafter", + "craftily", + "craftsman", + "craftwork", + "crafty", + "cramp", + "cranberry", + "crane", + "cranial", + "cranium", + "crank", + "crate", + "crave", + "craving", + "crawfish", + "crawlers", + "crawling", + "crayfish", + "crayon", + "crazed", + "crazily", + "craziness", + "crazy", + "creamed", + "creamer", + "creamlike", + "crease", + "creasing", + "creatable", + "create", + "creation", + "creative", + "creature", + "credible", + "credibly", + "credit", + "creed", + "creme", + "creole", + "crepe", + "crept", + "crescent", + "crested", + "cresting", + "crestless", + "crevice", + "crewless", + "crewman", + "crewmate", + "crib", + "cricket", + "cried", + "crier", + "crimp", + "crimson", + "cringe", + "cringing", + "crinkle", + "crinkly", + "crisped", + "crisping", + "crisply", + "crispness", + "crispy", + "criteria", + "critter", + "croak", + "crock", + "crook", + "croon", + "crop", + "cross", + "crouch", + "crouton", + "crowbar", + "crowd", + "crown", + "crucial", + "crudely", + "crudeness", + "cruelly", + "cruelness", + "cruelty", + "crumb", + "crummiest", + "crummy", + "crumpet", + "crumpled", + "cruncher", + "crunching", + "crunchy", + "crusader", + "crushable", + "crushed", + "crusher", + "crushing", + "crust", + "crux", + "crying", + "cryptic", + "crystal", + "cubbyhole", + "cube", + "cubical", + "cubicle", + "cucumber", + "cuddle", + "cuddly", + "cufflink", + "culinary", + "culminate", + "culpable", + "culprit", + "cultivate", + "cultural", + "culture", + "cupbearer", + "cupcake", + "cupid", + "cupped", + "cupping", + "curable", + "curator", + "curdle", + "cure", + "curfew", + "curing", + "curled", + "curler", + "curliness", + "curling", + "curly", + "curry", + "curse", + "cursive", + "cursor", + "curtain", + "curtly", + "curtsy", + "curvature", + "curve", + "curvy", + "cushy", + "cusp", + "cussed", + "custard", + "custodian", + "custody", + "customary", + "customer", + "customize", + "customs", + "cut", + "cycle", + "cyclic", + "cycling", + "cyclist", + "cylinder", + "cymbal", + "cytoplasm", + "cytoplast", + "dab", + "dad", + "daffodil", + "dagger", + "daily", + "daintily", + "dainty", + "dairy", + "daisy", + "dallying", + "dance", + "dancing", + "dandelion", + "dander", + "dandruff", + "dandy", + "danger", + "dangle", + "dangling", + "daredevil", + "dares", + "daringly", + "darkened", + "darkening", + "darkish", + "darkness", + "darkroom", + "darling", + "darn", + "dart", + "darwinism", + "dash", + "dastardly", + "data", + "datebook", + "dating", + "daughter", + "daunting", + "dawdler", + "dawn", + "daybed", + "daybreak", + "daycare", + "daydream", + "daylight", + "daylong", + "dayroom", + "daytime", + "dazzler", + "dazzling", + "deacon", + "deafening", + "deafness", + "dealer", + "dealing", + "dealmaker", + "dealt", + "dean", + "debatable", + "debate", + "debating", + "debit", + "debrief", + "debtless", + "debtor", + "debug", + "debunk", + "decade", + "decaf", + "decal", + "decathlon", + "decay", + "deceased", + "deceit", + "deceiver", + "deceiving", + "december", + "decency", + "decent", + "deception", + "deceptive", + "decibel", + "decidable", + "decimal", + "decimeter", + "decipher", + "deck", + "declared", + "decline", + "decode", + "decompose", + "decorated", + "decorator", + "decoy", + "decrease", + "decree", + "dedicate", + "dedicator", + "deduce", + "deduct", + "deed", + "deem", + "deepen", + "deeply", + "deepness", + "deface", + "defacing", + "defame", + "default", + "defeat", + "defection", + "defective", + "defendant", + "defender", + "defense", + "defensive", + "deferral", + "deferred", + "defiance", + "defiant", + "defile", + "defiling", + "define", + "definite", + "deflate", + "deflation", + "deflator", + "deflected", + "deflector", + "defog", + "deforest", + "defraud", + "defrost", + "deftly", + "defuse", + "defy", + "degraded", + "degrading", + "degrease", + "degree", + "dehydrate", + "deity", + "dejected", + "delay", + "delegate", + "delegator", + "delete", + "deletion", + "delicacy", + "delicate", + "delicious", + "delighted", + "delirious", + "delirium", + "deliverer", + "delivery", + "delouse", + "delta", + "deluge", + "delusion", + "deluxe", + "demanding", + "demeaning", + "demeanor", + "demise", + "democracy", + "democrat", + "demote", + "demotion", + "demystify", + "denatured", + "deniable", + "denial", + "denim", + "denote", + "dense", + "density", + "dental", + "dentist", + "denture", + "deny", + "deodorant", + "deodorize", + "departed", + "departure", + "depict", + "deplete", + "depletion", + "deplored", + "deploy", + "deport", + "depose", + "depraved", + "depravity", + "deprecate", + "depress", + "deprive", + "depth", + "deputize", + "deputy", + "derail", + "deranged", + "derby", + "derived", + "desecrate", + "deserve", + "deserving", + "designate", + "designed", + "designer", + "designing", + "deskbound", + "desktop", + "deskwork", + "desolate", + "despair", + "despise", + "despite", + "destiny", + "destitute", + "destruct", + "detached", + "detail", + "detection", + "detective", + "detector", + "detention", + "detergent", + "detest", + "detonate", + "detonator", + "detoxify", + "detract", + "deuce", + "devalue", + "deviancy", + "deviant", + "deviate", + "deviation", + "deviator", + "device", + "devious", + "devotedly", + "devotee", + "devotion", + "devourer", + "devouring", + "devoutly", + "dexterity", + "dexterous", + "diabetes", + "diabetic", + "diabolic", + "diagnoses", + "diagnosis", + "diagram", + "dial", + "diameter", + "diaper", + "diaphragm", + "diary", + "dice", + "dicing", + "dictate", + "dictation", + "dictator", + "difficult", + "diffused", + "diffuser", + "diffusion", + "diffusive", + "dig", + "dilation", + "diligence", + "diligent", + "dill", + "dilute", + "dime", + "diminish", + "dimly", + "dimmed", + "dimmer", + "dimness", + "dimple", + "diner", + "dingbat", + "dinghy", + "dinginess", + "dingo", + "dingy", + "dining", + "dinner", + "diocese", + "dioxide", + "diploma", + "dipped", + "dipper", + "dipping", + "directed", + "direction", + "directive", + "directly", + "directory", + "direness", + "dirtiness", + "disabled", + "disagree", + "disallow", + "disarm", + "disarray", + "disaster", + "disband", + "disbelief", + "disburse", + "discard", + "discern", + "discharge", + "disclose", + "discolor", + "discount", + "discourse", + "discover", + "discuss", + "disdain", + "disengage", + "disfigure", + "disgrace", + "dish", + "disinfect", + "disjoin", + "disk", + "dislike", + "disliking", + "dislocate", + "dislodge", + "disloyal", + "dismantle", + "dismay", + "dismiss", + "dismount", + "disobey", + "disorder", + "disown", + "disparate", + "disparity", + "dispatch", + "dispense", + "dispersal", + "dispersed", + "disperser", + "displace", + "display", + "displease", + "disposal", + "dispose", + "disprove", + "dispute", + "disregard", + "disrupt", + "dissuade", + "distance", + "distant", + "distaste", + "distill", + "distinct", + "distort", + "distract", + "distress", + "district", + "distrust", + "ditch", + "ditto", + "ditzy", + "dividable", + "divided", + "dividend", + "dividers", + "dividing", + "divinely", + "diving", + "divinity", + "divisible", + "divisibly", + "division", + "divisive", + "divorcee", + "dizziness", + "dizzy", + "doable", + "docile", + "dock", + "doctrine", + "document", + "dodge", + "dodgy", + "doily", + "doing", + "dole", + "dollar", + "dollhouse", + "dollop", + "dolly", + "dolphin", + "domain", + "domelike", + "domestic", + "dominion", + "dominoes", + "donated", + "donation", + "donator", + "donor", + "donut", + "doodle", + "doorbell", + "doorframe", + "doorknob", + "doorman", + "doormat", + "doornail", + "doorpost", + "doorstep", + "doorstop", + "doorway", + "doozy", + "dork", + "dormitory", + "dorsal", + "dosage", + "dose", + "dotted", + "doubling", + "douche", + "dove", + "down", + "dowry", + "doze", + "drab", + "dragging", + "dragonfly", + "dragonish", + "dragster", + "drainable", + "drainage", + "drained", + "drainer", + "drainpipe", + "dramatic", + "dramatize", + "drank", + "drapery", + "drastic", + "draw", + "dreaded", + "dreadful", + "dreadlock", + "dreamboat", + "dreamily", + "dreamland", + "dreamless", + "dreamlike", + "dreamt", + "dreamy", + "drearily", + "dreary", + "drench", + "dress", + "drew", + "dribble", + "dried", + "drier", + "drift", + "driller", + "drilling", + "drinkable", + "drinking", + "dripping", + "drippy", + "drivable", + "driven", + "driver", + "driveway", + "driving", + "drizzle", + "drizzly", + "drone", + "drool", + "droop", + "drop-down", + "dropbox", + "dropkick", + "droplet", + "dropout", + "dropper", + "drove", + "drown", + "drowsily", + "drudge", + "drum", + "dry", + "dubbed", + "dubiously", + "duchess", + "duckbill", + "ducking", + "duckling", + "ducktail", + "ducky", + "duct", + "dude", + "duffel", + "dugout", + "duh", + "duke", + "duller", + "dullness", + "duly", + "dumping", + "dumpling", + "dumpster", + "duo", + "dupe", + "duplex", + "duplicate", + "duplicity", + "durable", + "durably", + "duration", + "duress", + "during", + "dusk", + "dust", + "dutiful", + "duty", + "duvet", + "dwarf", + "dweeb", + "dwelled", + "dweller", + "dwelling", + "dwindle", + "dwindling", + "dynamic", + "dynamite", + "dynasty", + "dyslexia", + "dyslexic", + "each", + "eagle", + "earache", + "eardrum", + "earflap", + "earful", + "earlobe", + "early", + "earmark", + "earmuff", + "earphone", + "earpiece", + "earplugs", + "earring", + "earshot", + "earthen", + "earthlike", + "earthling", + "earthly", + "earthworm", + "earthy", + "earwig", + "easeful", + "easel", + "easiest", + "easily", + "easiness", + "easing", + "eastbound", + "eastcoast", + "easter", + "eastward", + "eatable", + "eaten", + "eatery", + "eating", + "eats", + "ebay", + "ebony", + "ebook", + "ecard", + "eccentric", + "echo", + "eclair", + "eclipse", + "ecologist", + "ecology", + "economic", + "economist", + "economy", + "ecosphere", + "ecosystem", + "edge", + "edginess", + "edging", + "edgy", + "edition", + "editor", + "educated", + "education", + "educator", + "eel", + "effective", + "effects", + "efficient", + "effort", + "eggbeater", + "egging", + "eggnog", + "eggplant", + "eggshell", + "egomaniac", + "egotism", + "egotistic", + "either", + "eject", + "elaborate", + "elastic", + "elated", + "elbow", + "eldercare", + "elderly", + "eldest", + "electable", + "election", + "elective", + "elephant", + "elevate", + "elevating", + "elevation", + "elevator", + "eleven", + "elf", + "eligible", + "eligibly", + "eliminate", + "elite", + "elitism", + "elixir", + "elk", + "ellipse", + "elliptic", + "elm", + "elongated", + "elope", + "eloquence", + "eloquent", + "elsewhere", + "elude", + "elusive", + "elves", + "email", + "embargo", + "embark", + "embassy", + "embattled", + "embellish", + "ember", + "embezzle", + "emblaze", + "emblem", + "embody", + "embolism", + "emboss", + "embroider", + "emcee", + "emerald", + "emergency", + "emission", + "emit", + "emote", + "emoticon", + "emotion", + "empathic", + "empathy", + "emperor", + "emphases", + "emphasis", + "emphasize", + "emphatic", + "empirical", + "employed", + "employee", + "employer", + "emporium", + "empower", + "emptier", + "emptiness", + "empty", + "emu", + "enable", + "enactment", + "enamel", + "enchanted", + "enchilada", + "encircle", + "enclose", + "enclosure", + "encode", + "encore", + "encounter", + "encourage", + "encroach", + "encrust", + "encrypt", + "endanger", + "endeared", + "endearing", + "ended", + "ending", + "endless", + "endnote", + "endocrine", + "endorphin", + "endorse", + "endowment", + "endpoint", + "endurable", + "endurance", + "enduring", + "energetic", + "energize", + "energy", + "enforced", + "enforcer", + "engaged", + "engaging", + "engine", + "engorge", + "engraved", + "engraver", + "engraving", + "engross", + "engulf", + "enhance", + "enigmatic", + "enjoyable", + "enjoyably", + "enjoyer", + "enjoying", + "enjoyment", + "enlarged", + "enlarging", + "enlighten", + "enlisted", + "enquirer", + "enrage", + "enrich", + "enroll", + "enslave", + "ensnare", + "ensure", + "entail", + "entangled", + "entering", + "entertain", + "enticing", + "entire", + "entitle", + "entity", + "entomb", + "entourage", + "entrap", + "entree", + "entrench", + "entrust", + "entryway", + "entwine", + "enunciate", + "envelope", + "enviable", + "enviably", + "envious", + "envision", + "envoy", + "envy", + "enzyme", + "epic", + "epidemic", + "epidermal", + "epidermis", + "epidural", + "epilepsy", + "epileptic", + "epilogue", + "epiphany", + "episode", + "equal", + "equate", + "equation", + "equator", + "equinox", + "equipment", + "equity", + "equivocal", + "eradicate", + "erasable", + "erased", + "eraser", + "erasure", + "ergonomic", + "errand", + "errant", + "erratic", + "error", + "erupt", + "escalate", + "escalator", + "escapable", + "escapade", + "escapist", + "escargot", + "eskimo", + "esophagus", + "espionage", + "espresso", + "esquire", + "essay", + "essence", + "essential", + "establish", + "estate", + "esteemed", + "estimate", + "estimator", + "estranged", + "estrogen", + "etching", + "eternal", + "eternity", + "ethanol", + "ether", + "ethically", + "ethics", + "euphemism", + "evacuate", + "evacuee", + "evade", + "evaluate", + "evaluator", + "evaporate", + "evasion", + "evasive", + "even", + "everglade", + "evergreen", + "everybody", + "everyday", + "everyone", + "evict", + "evidence", + "evident", + "evil", + "evoke", + "evolution", + "evolve", + "exact", + "exalted", + "example", + "excavate", + "excavator", + "exceeding", + "exception", + "excess", + "exchange", + "excitable", + "exciting", + "exclaim", + "exclude", + "excluding", + "exclusion", + "exclusive", + "excretion", + "excretory", + "excursion", + "excusable", + "excusably", + "excuse", + "exemplary", + "exemplify", + "exemption", + "exerciser", + "exert", + "exes", + "exfoliate", + "exhale", + "exhaust", + "exhume", + "exile", + "existing", + "exit", + "exodus", + "exonerate", + "exorcism", + "exorcist", + "expand", + "expanse", + "expansion", + "expansive", + "expectant", + "expedited", + "expediter", + "expel", + "expend", + "expenses", + "expensive", + "expert", + "expire", + "expiring", + "explain", + "expletive", + "explicit", + "explode", + "exploit", + "explore", + "exploring", + "exponent", + "exporter", + "exposable", + "expose", + "exposure", + "express", + "expulsion", + "exquisite", + "extended", + "extending", + "extent", + "extenuate", + "exterior", + "external", + "extinct", + "extortion", + "extradite", + "extras", + "extrovert", + "extrude", + "extruding", + "exuberant", + "fable", + "fabric", + "fabulous", + "facebook", + "facecloth", + "facedown", + "faceless", + "facelift", + "faceplate", + "faceted", + "facial", + "facility", + "facing", + "facsimile", + "faction", + "factoid", + "factor", + "factsheet", + "factual", + "faculty", + "fade", + "fading", + "failing", + "falcon", + "fall", + "false", + "falsify", + "fame", + "familiar", + "family", + "famine", + "famished", + "fanatic", + "fancied", + "fanciness", + "fancy", + "fanfare", + "fang", + "fanning", + "fantasize", + "fantastic", + "fantasy", + "fascism", + "fastball", + "faster", + "fasting", + "fastness", + "faucet", + "favorable", + "favorably", + "favored", + "favoring", + "favorite", + "fax", + "feast", + "federal", + "fedora", + "feeble", + "feed", + "feel", + "feisty", + "feline", + "felt-tip", + "feminine", + "feminism", + "feminist", + "feminize", + "femur", + "fence", + "fencing", + "fender", + "ferment", + "fernlike", + "ferocious", + "ferocity", + "ferret", + "ferris", + "ferry", + "fervor", + "fester", + "festival", + "festive", + "festivity", + "fetal", + "fetch", + "fever", + "fiber", + "fiction", + "fiddle", + "fiddling", + "fidelity", + "fidgeting", + "fidgety", + "fifteen", + "fifth", + "fiftieth", + "fifty", + "figment", + "figure", + "figurine", + "filing", + "filled", + "filler", + "filling", + "film", + "filter", + "filth", + "filtrate", + "finale", + "finalist", + "finalize", + "finally", + "finance", + "financial", + "finch", + "fineness", + "finer", + "finicky", + "finished", + "finisher", + "finishing", + "finite", + "finless", + "finlike", + "fiscally", + "fit", + "five", + "flaccid", + "flagman", + "flagpole", + "flagship", + "flagstick", + "flagstone", + "flail", + "flakily", + "flaky", + "flame", + "flammable", + "flanked", + "flanking", + "flannels", + "flap", + "flaring", + "flashback", + "flashbulb", + "flashcard", + "flashily", + "flashing", + "flashy", + "flask", + "flatbed", + "flatfoot", + "flatly", + "flatness", + "flatten", + "flattered", + "flatterer", + "flattery", + "flattop", + "flatware", + "flatworm", + "flavored", + "flavorful", + "flavoring", + "flaxseed", + "fled", + "fleshed", + "fleshy", + "flick", + "flier", + "flight", + "flinch", + "fling", + "flint", + "flip", + "flirt", + "float", + "flock", + "flogging", + "flop", + "floral", + "florist", + "floss", + "flounder", + "flyable", + "flyaway", + "flyer", + "flying", + "flyover", + "flypaper", + "foam", + "foe", + "fog", + "foil", + "folic", + "folk", + "follicle", + "follow", + "fondling", + "fondly", + "fondness", + "fondue", + "font", + "food", + "fool", + "footage", + "football", + "footbath", + "footboard", + "footer", + "footgear", + "foothill", + "foothold", + "footing", + "footless", + "footman", + "footnote", + "footpad", + "footpath", + "footprint", + "footrest", + "footsie", + "footsore", + "footwear", + "footwork", + "fossil", + "foster", + "founder", + "founding", + "fountain", + "fox", + "foyer", + "fraction", + "fracture", + "fragile", + "fragility", + "fragment", + "fragrance", + "fragrant", + "frail", + "frame", + "framing", + "frantic", + "fraternal", + "frayed", + "fraying", + "frays", + "freckled", + "freckles", + "freebase", + "freebee", + "freebie", + "freedom", + "freefall", + "freehand", + "freeing", + "freeload", + "freely", + "freemason", + "freeness", + "freestyle", + "freeware", + "freeway", + "freewill", + "freezable", + "freezing", + "freight", + "french", + "frenzied", + "frenzy", + "frequency", + "frequent", + "fresh", + "fretful", + "fretted", + "friction", + "friday", + "fridge", + "fried", + "friend", + "frighten", + "frightful", + "frigidity", + "frigidly", + "frill", + "fringe", + "frisbee", + "frisk", + "fritter", + "frivolous", + "frolic", + "from", + "front", + "frostbite", + "frosted", + "frostily", + "frosting", + "frostlike", + "frosty", + "froth", + "frown", + "frozen", + "fructose", + "frugality", + "frugally", + "fruit", + "frustrate", + "frying", + "gab", + "gaffe", + "gag", + "gainfully", + "gaining", + "gains", + "gala", + "gallantly", + "galleria", + "gallery", + "galley", + "gallon", + "gallows", + "gallstone", + "galore", + "galvanize", + "gambling", + "game", + "gaming", + "gamma", + "gander", + "gangly", + "gangrene", + "gangway", + "gap", + "garage", + "garbage", + "garden", + "gargle", + "garland", + "garlic", + "garment", + "garnet", + "garnish", + "garter", + "gas", + "gatherer", + "gathering", + "gating", + "gauging", + "gauntlet", + "gauze", + "gave", + "gawk", + "gazing", + "gear", + "gecko", + "geek", + "geiger", + "gem", + "gender", + "generic", + "generous", + "genetics", + "genre", + "gentile", + "gentleman", + "gently", + "gents", + "geography", + "geologic", + "geologist", + "geology", + "geometric", + "geometry", + "geranium", + "gerbil", + "geriatric", + "germicide", + "germinate", + "germless", + "germproof", + "gestate", + "gestation", + "gesture", + "getaway", + "getting", + "getup", + "giant", + "gibberish", + "giblet", + "giddily", + "giddiness", + "giddy", + "gift", + "gigabyte", + "gigahertz", + "gigantic", + "giggle", + "giggling", + "giggly", + "gigolo", + "gilled", + "gills", + "gimmick", + "girdle", + "giveaway", + "given", + "giver", + "giving", + "gizmo", + "gizzard", + "glacial", + "glacier", + "glade", + "gladiator", + "gladly", + "glamorous", + "glamour", + "glance", + "glancing", + "glandular", + "glare", + "glaring", + "glass", + "glaucoma", + "glazing", + "gleaming", + "gleeful", + "glider", + "gliding", + "glimmer", + "glimpse", + "glisten", + "glitch", + "glitter", + "glitzy", + "gloater", + "gloating", + "gloomily", + "gloomy", + "glorified", + "glorifier", + "glorify", + "glorious", + "glory", + "gloss", + "glove", + "glowing", + "glowworm", + "glucose", + "glue", + "gluten", + "glutinous", + "glutton", + "gnarly", + "gnat", + "goal", + "goatskin", + "goes", + "goggles", + "going", + "goldfish", + "goldmine", + "goldsmith", + "golf", + "goliath", + "gonad", + "gondola", + "gone", + "gong", + "good", + "gooey", + "goofball", + "goofiness", + "goofy", + "google", + "goon", + "gopher", + "gore", + "gorged", + "gorgeous", + "gory", + "gosling", + "gossip", + "gothic", + "gotten", + "gout", + "gown", + "grab", + "graceful", + "graceless", + "gracious", + "gradation", + "graded", + "grader", + "gradient", + "grading", + "gradually", + "graduate", + "graffiti", + "grafted", + "grafting", + "grain", + "granddad", + "grandkid", + "grandly", + "grandma", + "grandpa", + "grandson", + "granite", + "granny", + "granola", + "grant", + "granular", + "grape", + "graph", + "grapple", + "grappling", + "grasp", + "grass", + "gratified", + "gratify", + "grating", + "gratitude", + "gratuity", + "gravel", + "graveness", + "graves", + "graveyard", + "gravitate", + "gravity", + "gravy", + "gray", + "grazing", + "greasily", + "greedily", + "greedless", + "greedy", + "green", + "greeter", + "greeting", + "grew", + "greyhound", + "grid", + "grief", + "grievance", + "grieving", + "grievous", + "grill", + "grimace", + "grimacing", + "grime", + "griminess", + "grimy", + "grinch", + "grinning", + "grip", + "gristle", + "grit", + "groggily", + "groggy", + "groin", + "groom", + "groove", + "grooving", + "groovy", + "grope", + "ground", + "grouped", + "grout", + "grove", + "grower", + "growing", + "growl", + "grub", + "grudge", + "grudging", + "grueling", + "gruffly", + "grumble", + "grumbling", + "grumbly", + "grumpily", + "grunge", + "grunt", + "guacamole", + "guidable", + "guidance", + "guide", + "guiding", + "guileless", + "guise", + "gulf", + "gullible", + "gully", + "gulp", + "gumball", + "gumdrop", + "gumminess", + "gumming", + "gummy", + "gurgle", + "gurgling", + "guru", + "gush", + "gusto", + "gusty", + "gutless", + "guts", + "gutter", + "guy", + "guzzler", + "gyration", + "habitable", + "habitant", + "habitat", + "habitual", + "hacked", + "hacker", + "hacking", + "hacksaw", + "had", + "haggler", + "haiku", + "half", + "halogen", + "halt", + "halved", + "halves", + "hamburger", + "hamlet", + "hammock", + "hamper", + "hamster", + "hamstring", + "handbag", + "handball", + "handbook", + "handbrake", + "handcart", + "handclap", + "handclasp", + "handcraft", + "handcuff", + "handed", + "handful", + "handgrip", + "handgun", + "handheld", + "handiness", + "handiwork", + "handlebar", + "handled", + "handler", + "handling", + "handmade", + "handoff", + "handpick", + "handprint", + "handrail", + "handsaw", + "handset", + "handsfree", + "handshake", + "handstand", + "handwash", + "handwork", + "handwoven", + "handwrite", + "handyman", + "hangnail", + "hangout", + "hangover", + "hangup", + "hankering", + "hankie", + "hanky", + "haphazard", + "happening", + "happier", + "happiest", + "happily", + "happiness", + "happy", + "harbor", + "hardcopy", + "hardcore", + "hardcover", + "harddisk", + "hardened", + "hardener", + "hardening", + "hardhat", + "hardhead", + "hardiness", + "hardly", + "hardness", + "hardship", + "hardware", + "hardwired", + "hardwood", + "hardy", + "harmful", + "harmless", + "harmonica", + "harmonics", + "harmonize", + "harmony", + "harness", + "harpist", + "harsh", + "harvest", + "hash", + "hassle", + "haste", + "hastily", + "hastiness", + "hasty", + "hatbox", + "hatchback", + "hatchery", + "hatchet", + "hatching", + "hatchling", + "hate", + "hatless", + "hatred", + "haunt", + "haven", + "hazard", + "hazelnut", + "hazily", + "haziness", + "hazing", + "hazy", + "headache", + "headband", + "headboard", + "headcount", + "headdress", + "headed", + "header", + "headfirst", + "headgear", + "heading", + "headlamp", + "headless", + "headlock", + "headphone", + "headpiece", + "headrest", + "headroom", + "headscarf", + "headset", + "headsman", + "headstand", + "headstone", + "headway", + "headwear", + "heap", + "heat", + "heave", + "heavily", + "heaviness", + "heaving", + "hedge", + "hedging", + "heftiness", + "hefty", + "helium", + "helmet", + "helper", + "helpful", + "helping", + "helpless", + "helpline", + "hemlock", + "hemstitch", + "hence", + "henchman", + "henna", + "herald", + "herbal", + "herbicide", + "herbs", + "heritage", + "hermit", + "heroics", + "heroism", + "herring", + "herself", + "hertz", + "hesitancy", + "hesitant", + "hesitate", + "hexagon", + "hexagram", + "hubcap", + "huddle", + "huddling", + "huff", + "hug", + "hula", + "hulk", + "hull", + "human", + "humble", + "humbling", + "humbly", + "humid", + "humiliate", + "humility", + "humming", + "hummus", + "humongous", + "humorist", + "humorless", + "humorous", + "humpback", + "humped", + "humvee", + "hunchback", + "hundredth", + "hunger", + "hungrily", + "hungry", + "hunk", + "hunter", + "hunting", + "huntress", + "huntsman", + "hurdle", + "hurled", + "hurler", + "hurling", + "hurray", + "hurricane", + "hurried", + "hurry", + "hurt", + "husband", + "hush", + "husked", + "huskiness", + "hut", + "hybrid", + "hydrant", + "hydrated", + "hydration", + "hydrogen", + "hydroxide", + "hyperlink", + "hypertext", + "hyphen", + "hypnoses", + "hypnosis", + "hypnotic", + "hypnotism", + "hypnotist", + "hypnotize", + "hypocrisy", + "hypocrite", + "ibuprofen", + "ice", + "iciness", + "icing", + "icky", + "icon", + "icy", + "idealism", + "idealist", + "idealize", + "ideally", + "idealness", + "identical", + "identify", + "identity", + "ideology", + "idiocy", + "idiom", + "idly", + "igloo", + "ignition", + "ignore", + "iguana", + "illicitly", + "illusion", + "illusive", + "image", + "imaginary", + "imagines", + "imaging", + "imbecile", + "imitate", + "imitation", + "immature", + "immerse", + "immersion", + "imminent", + "immobile", + "immodest", + "immorally", + "immortal", + "immovable", + "immovably", + "immunity", + "immunize", + "impaired", + "impale", + "impart", + "impatient", + "impeach", + "impeding", + "impending", + "imperfect", + "imperial", + "impish", + "implant", + "implement", + "implicate", + "implicit", + "implode", + "implosion", + "implosive", + "imply", + "impolite", + "important", + "importer", + "impose", + "imposing", + "impotence", + "impotency", + "impotent", + "impound", + "imprecise", + "imprint", + "imprison", + "impromptu", + "improper", + "improve", + "improving", + "improvise", + "imprudent", + "impulse", + "impulsive", + "impure", + "impurity", + "iodine", + "iodize", + "ion", + "ipad", + "iphone", + "ipod", + "irate", + "irk", + "iron", + "irregular", + "irrigate", + "irritable", + "irritably", + "irritant", + "irritate", + "islamic", + "islamist", + "isolated", + "isolating", + "isolation", + "isotope", + "issue", + "issuing", + "italicize", + "italics", + "item", + "itinerary", + "itunes", + "ivory", + "ivy", + "jab", + "jackal", + "jacket", + "jackknife", + "jackpot", + "jailbird", + "jailbreak", + "jailer", + "jailhouse", + "jalapeno", + "jam", + "janitor", + "january", + "jargon", + "jarring", + "jasmine", + "jaundice", + "jaunt", + "java", + "jawed", + "jawless", + "jawline", + "jaws", + "jaybird", + "jaywalker", + "jazz", + "jeep", + "jeeringly", + "jellied", + "jelly", + "jersey", + "jester", + "jet", + "jiffy", + "jigsaw", + "jimmy", + "jingle", + "jingling", + "jinx", + "jitters", + "jittery", + "job", + "jockey", + "jockstrap", + "jogger", + "jogging", + "john", + "joining", + "jokester", + "jokingly", + "jolliness", + "jolly", + "jolt", + "jot", + "jovial", + "joyfully", + "joylessly", + "joyous", + "joyride", + "joystick", + "jubilance", + "jubilant", + "judge", + "judgingly", + "judicial", + "judiciary", + "judo", + "juggle", + "juggling", + "jugular", + "juice", + "juiciness", + "juicy", + "jujitsu", + "jukebox", + "july", + "jumble", + "jumbo", + "jump", + "junction", + "juncture", + "june", + "junior", + "juniper", + "junkie", + "junkman", + "junkyard", + "jurist", + "juror", + "jury", + "justice", + "justifier", + "justify", + "justly", + "justness", + "juvenile", + "kabob", + "kangaroo", + "karaoke", + "karate", + "karma", + "kebab", + "keenly", + "keenness", + "keep", + "keg", + "kelp", + "kennel", + "kept", + "kerchief", + "kerosene", + "kettle", + "kick", + "kiln", + "kilobyte", + "kilogram", + "kilometer", + "kilowatt", + "kilt", + "kimono", + "kindle", + "kindling", + "kindly", + "kindness", + "kindred", + "kinetic", + "kinfolk", + "king", + "kinship", + "kinsman", + "kinswoman", + "kissable", + "kisser", + "kissing", + "kitchen", + "kite", + "kitten", + "kitty", + "kiwi", + "kleenex", + "knapsack", + "knee", + "knelt", + "knickers", + "knoll", + "koala", + "kooky", + "kosher", + "krypton", + "kudos", + "kung", + "labored", + "laborer", + "laboring", + "laborious", + "labrador", + "ladder", + "ladies", + "ladle", + "ladybug", + "ladylike", + "lagged", + "lagging", + "lagoon", + "lair", + "lake", + "lance", + "landed", + "landfall", + "landfill", + "landing", + "landlady", + "landless", + "landline", + "landlord", + "landmark", + "landmass", + "landmine", + "landowner", + "landscape", + "landside", + "landslide", + "language", + "lankiness", + "lanky", + "lantern", + "lapdog", + "lapel", + "lapped", + "lapping", + "laptop", + "lard", + "large", + "lark", + "lash", + "lasso", + "last", + "latch", + "late", + "lather", + "latitude", + "latrine", + "latter", + "latticed", + "launch", + "launder", + "laundry", + "laurel", + "lavender", + "lavish", + "laxative", + "lazily", + "laziness", + "lazy", + "lecturer", + "left", + "legacy", + "legal", + "legend", + "legged", + "leggings", + "legible", + "legibly", + "legislate", + "lego", + "legroom", + "legume", + "legwarmer", + "legwork", + "lemon", + "lend", + "length", + "lens", + "lent", + "leotard", + "lesser", + "letdown", + "lethargic", + "lethargy", + "letter", + "lettuce", + "level", + "leverage", + "levers", + "levitate", + "levitator", + "liability", + "liable", + "liberty", + "librarian", + "library", + "licking", + "licorice", + "lid", + "life", + "lifter", + "lifting", + "liftoff", + "ligament", + "likely", + "likeness", + "likewise", + "liking", + "lilac", + "lilly", + "lily", + "limb", + "limeade", + "limelight", + "limes", + "limit", + "limping", + "limpness", + "line", + "lingo", + "linguini", + "linguist", + "lining", + "linked", + "linoleum", + "linseed", + "lint", + "lion", + "lip", + "liquefy", + "liqueur", + "liquid", + "lisp", + "list", + "litigate", + "litigator", + "litmus", + "litter", + "little", + "livable", + "lived", + "lively", + "liver", + "livestock", + "lividly", + "living", + "lizard", + "lubricant", + "lubricate", + "lucid", + "luckily", + "luckiness", + "luckless", + "lucrative", + "ludicrous", + "lugged", + "lukewarm", + "lullaby", + "lumber", + "luminance", + "luminous", + "lumpiness", + "lumping", + "lumpish", + "lunacy", + "lunar", + "lunchbox", + "luncheon", + "lunchroom", + "lunchtime", + "lung", + "lurch", + "lure", + "luridness", + "lurk", + "lushly", + "lushness", + "luster", + "lustfully", + "lustily", + "lustiness", + "lustrous", + "lusty", + "luxurious", + "luxury", + "lying", + "lyrically", + "lyricism", + "lyricist", + "lyrics", + "macarena", + "macaroni", + "macaw", + "mace", + "machine", + "machinist", + "magazine", + "magenta", + "maggot", + "magical", + "magician", + "magma", + "magnesium", + "magnetic", + "magnetism", + "magnetize", + "magnifier", + "magnify", + "magnitude", + "magnolia", + "mahogany", + "maimed", + "majestic", + "majesty", + "majorette", + "majority", + "makeover", + "maker", + "makeshift", + "making", + "malformed", + "malt", + "mama", + "mammal", + "mammary", + "mammogram", + "manager", + "managing", + "manatee", + "mandarin", + "mandate", + "mandatory", + "mandolin", + "manger", + "mangle", + "mango", + "mangy", + "manhandle", + "manhole", + "manhood", + "manhunt", + "manicotti", + "manicure", + "manifesto", + "manila", + "mankind", + "manlike", + "manliness", + "manly", + "manmade", + "manned", + "mannish", + "manor", + "manpower", + "mantis", + "mantra", + "manual", + "many", + "map", + "marathon", + "marauding", + "marbled", + "marbles", + "marbling", + "march", + "mardi", + "margarine", + "margarita", + "margin", + "marigold", + "marina", + "marine", + "marital", + "maritime", + "marlin", + "marmalade", + "maroon", + "married", + "marrow", + "marry", + "marshland", + "marshy", + "marsupial", + "marvelous", + "marxism", + "mascot", + "masculine", + "mashed", + "mashing", + "massager", + "masses", + "massive", + "mastiff", + "matador", + "matchbook", + "matchbox", + "matcher", + "matching", + "matchless", + "material", + "maternal", + "maternity", + "math", + "mating", + "matriarch", + "matrimony", + "matrix", + "matron", + "matted", + "matter", + "maturely", + "maturing", + "maturity", + "mauve", + "maverick", + "maximize", + "maximum", + "maybe", + "mayday", + "mayflower", + "moaner", + "moaning", + "mobile", + "mobility", + "mobilize", + "mobster", + "mocha", + "mocker", + "mockup", + "modified", + "modify", + "modular", + "modulator", + "module", + "moisten", + "moistness", + "moisture", + "molar", + "molasses", + "mold", + "molecular", + "molecule", + "molehill", + "mollusk", + "mom", + "monastery", + "monday", + "monetary", + "monetize", + "moneybags", + "moneyless", + "moneywise", + "mongoose", + "mongrel", + "monitor", + "monkhood", + "monogamy", + "monogram", + "monologue", + "monopoly", + "monorail", + "monotone", + "monotype", + "monoxide", + "monsieur", + "monsoon", + "monstrous", + "monthly", + "monument", + "moocher", + "moodiness", + "moody", + "mooing", + "moonbeam", + "mooned", + "moonlight", + "moonlike", + "moonlit", + "moonrise", + "moonscape", + "moonshine", + "moonstone", + "moonwalk", + "mop", + "morale", + "morality", + "morally", + "morbidity", + "morbidly", + "morphine", + "morphing", + "morse", + "mortality", + "mortally", + "mortician", + "mortified", + "mortify", + "mortuary", + "mosaic", + "mossy", + "most", + "mothball", + "mothproof", + "motion", + "motivate", + "motivator", + "motive", + "motocross", + "motor", + "motto", + "mountable", + "mountain", + "mounted", + "mounting", + "mourner", + "mournful", + "mouse", + "mousiness", + "moustache", + "mousy", + "mouth", + "movable", + "move", + "movie", + "moving", + "mower", + "mowing", + "much", + "muck", + "mud", + "mug", + "mulberry", + "mulch", + "mule", + "mulled", + "mullets", + "multiple", + "multiply", + "multitask", + "multitude", + "mumble", + "mumbling", + "mumbo", + "mummified", + "mummify", + "mummy", + "mumps", + "munchkin", + "mundane", + "municipal", + "muppet", + "mural", + "murkiness", + "murky", + "murmuring", + "muscular", + "museum", + "mushily", + "mushiness", + "mushroom", + "mushy", + "music", + "musket", + "muskiness", + "musky", + "mustang", + "mustard", + "muster", + "mustiness", + "musty", + "mutable", + "mutate", + "mutation", + "mute", + "mutilated", + "mutilator", + "mutiny", + "mutt", + "mutual", + "muzzle", + "myself", + "myspace", + "mystified", + "mystify", + "myth", + "nacho", + "nag", + "nail", + "name", + "naming", + "nanny", + "nanometer", + "nape", + "napkin", + "napped", + "napping", + "nappy", + "narrow", + "nastily", + "nastiness", + "national", + "native", + "nativity", + "natural", + "nature", + "naturist", + "nautical", + "navigate", + "navigator", + "navy", + "nearby", + "nearest", + "nearly", + "nearness", + "neatly", + "neatness", + "nebula", + "nebulizer", + "nectar", + "negate", + "negation", + "negative", + "neglector", + "negligee", + "negligent", + "negotiate", + "nemeses", + "nemesis", + "neon", + "nephew", + "nerd", + "nervous", + "nervy", + "nest", + "net", + "neurology", + "neuron", + "neurosis", + "neurotic", + "neuter", + "neutron", + "never", + "next", + "nibble", + "nickname", + "nicotine", + "niece", + "nifty", + "nimble", + "nimbly", + "nineteen", + "ninetieth", + "ninja", + "nintendo", + "ninth", + "nuclear", + "nuclei", + "nucleus", + "nugget", + "nullify", + "number", + "numbing", + "numbly", + "numbness", + "numeral", + "numerate", + "numerator", + "numeric", + "numerous", + "nuptials", + "nursery", + "nursing", + "nurture", + "nutcase", + "nutlike", + "nutmeg", + "nutrient", + "nutshell", + "nuttiness", + "nutty", + "nuzzle", + "nylon", + "oaf", + "oak", + "oasis", + "oat", + "obedience", + "obedient", + "obituary", + "object", + "obligate", + "obliged", + "oblivion", + "oblivious", + "oblong", + "obnoxious", + "oboe", + "obscure", + "obscurity", + "observant", + "observer", + "observing", + "obsessed", + "obsession", + "obsessive", + "obsolete", + "obstacle", + "obstinate", + "obstruct", + "obtain", + "obtrusive", + "obtuse", + "obvious", + "occultist", + "occupancy", + "occupant", + "occupier", + "occupy", + "ocean", + "ocelot", + "octagon", + "octane", + "october", + "octopus", + "ogle", + "oil", + "oink", + "ointment", + "okay", + "old", + "olive", + "olympics", + "omega", + "omen", + "ominous", + "omission", + "omit", + "omnivore", + "onboard", + "oncoming", + "ongoing", + "onion", + "online", + "onlooker", + "only", + "onscreen", + "onset", + "onshore", + "onslaught", + "onstage", + "onto", + "onward", + "onyx", + "oops", + "ooze", + "oozy", + "opacity", + "opal", + "open", + "operable", + "operate", + "operating", + "operation", + "operative", + "operator", + "opium", + "opossum", + "opponent", + "oppose", + "opposing", + "opposite", + "oppressed", + "oppressor", + "opt", + "opulently", + "osmosis", + "other", + "otter", + "ouch", + "ought", + "ounce", + "outage", + "outback", + "outbid", + "outboard", + "outbound", + "outbreak", + "outburst", + "outcast", + "outclass", + "outcome", + "outdated", + "outdoors", + "outer", + "outfield", + "outfit", + "outflank", + "outgoing", + "outgrow", + "outhouse", + "outing", + "outlast", + "outlet", + "outline", + "outlook", + "outlying", + "outmatch", + "outmost", + "outnumber", + "outplayed", + "outpost", + "outpour", + "output", + "outrage", + "outrank", + "outreach", + "outright", + "outscore", + "outsell", + "outshine", + "outshoot", + "outsider", + "outskirts", + "outsmart", + "outsource", + "outspoken", + "outtakes", + "outthink", + "outward", + "outweigh", + "outwit", + "oval", + "ovary", + "oven", + "overact", + "overall", + "overarch", + "overbid", + "overbill", + "overbite", + "overblown", + "overboard", + "overbook", + "overbuilt", + "overcast", + "overcoat", + "overcome", + "overcook", + "overcrowd", + "overdraft", + "overdrawn", + "overdress", + "overdrive", + "overdue", + "overeager", + "overeater", + "overexert", + "overfed", + "overfeed", + "overfill", + "overflow", + "overfull", + "overgrown", + "overhand", + "overhang", + "overhaul", + "overhead", + "overhear", + "overheat", + "overhung", + "overjoyed", + "overkill", + "overlabor", + "overlaid", + "overlap", + "overlay", + "overload", + "overlook", + "overlord", + "overlying", + "overnight", + "overpass", + "overpay", + "overplant", + "overplay", + "overpower", + "overprice", + "overrate", + "overreach", + "overreact", + "override", + "overripe", + "overrule", + "overrun", + "overshoot", + "overshot", + "oversight", + "oversized", + "oversleep", + "oversold", + "overspend", + "overstate", + "overstay", + "overstep", + "overstock", + "overstuff", + "oversweet", + "overtake", + "overthrow", + "overtime", + "overtly", + "overtone", + "overture", + "overturn", + "overuse", + "overvalue", + "overview", + "overwrite", + "owl", + "oxford", + "oxidant", + "oxidation", + "oxidize", + "oxidizing", + "oxygen", + "oxymoron", + "oyster", + "ozone", + "paced", + "pacemaker", + "pacific", + "pacifier", + "pacifism", + "pacifist", + "pacify", + "padded", + "padding", + "paddle", + "paddling", + "padlock", + "pagan", + "pager", + "paging", + "pajamas", + "palace", + "palatable", + "palm", + "palpable", + "palpitate", + "paltry", + "pampered", + "pamperer", + "pampers", + "pamphlet", + "panama", + "pancake", + "pancreas", + "panda", + "pandemic", + "pang", + "panhandle", + "panic", + "panning", + "panorama", + "panoramic", + "panther", + "pantomime", + "pantry", + "pants", + "pantyhose", + "paparazzi", + "papaya", + "paper", + "paprika", + "papyrus", + "parabola", + "parachute", + "parade", + "paradox", + "paragraph", + "parakeet", + "paralegal", + "paralyses", + "paralysis", + "paralyze", + "paramedic", + "parameter", + "paramount", + "parasail", + "parasite", + "parasitic", + "parcel", + "parched", + "parchment", + "pardon", + "parish", + "parka", + "parking", + "parkway", + "parlor", + "parmesan", + "parole", + "parrot", + "parsley", + "parsnip", + "partake", + "parted", + "parting", + "partition", + "partly", + "partner", + "partridge", + "party", + "passable", + "passably", + "passage", + "passcode", + "passenger", + "passerby", + "passing", + "passion", + "passive", + "passivism", + "passover", + "passport", + "password", + "pasta", + "pasted", + "pastel", + "pastime", + "pastor", + "pastrami", + "pasture", + "pasty", + "patchwork", + "patchy", + "paternal", + "paternity", + "path", + "patience", + "patient", + "patio", + "patriarch", + "patriot", + "patrol", + "patronage", + "patronize", + "pauper", + "pavement", + "paver", + "pavestone", + "pavilion", + "paving", + "pawing", + "payable", + "payback", + "paycheck", + "payday", + "payee", + "payer", + "paying", + "payment", + "payphone", + "payroll", + "pebble", + "pebbly", + "pecan", + "pectin", + "peculiar", + "peddling", + "pediatric", + "pedicure", + "pedigree", + "pedometer", + "pegboard", + "pelican", + "pellet", + "pelt", + "pelvis", + "penalize", + "penalty", + "pencil", + "pendant", + "pending", + "penholder", + "penknife", + "pennant", + "penniless", + "penny", + "penpal", + "pension", + "pentagon", + "pentagram", + "pep", + "perceive", + "percent", + "perch", + "percolate", + "perennial", + "perfected", + "perfectly", + "perfume", + "periscope", + "perish", + "perjurer", + "perjury", + "perkiness", + "perky", + "perm", + "peroxide", + "perpetual", + "perplexed", + "persecute", + "persevere", + "persuaded", + "persuader", + "pesky", + "peso", + "pessimism", + "pessimist", + "pester", + "pesticide", + "petal", + "petite", + "petition", + "petri", + "petroleum", + "petted", + "petticoat", + "pettiness", + "petty", + "petunia", + "phantom", + "phobia", + "phoenix", + "phonebook", + "phoney", + "phonics", + "phoniness", + "phony", + "phosphate", + "photo", + "phrase", + "phrasing", + "placard", + "placate", + "placidly", + "plank", + "planner", + "plant", + "plasma", + "plaster", + "plastic", + "plated", + "platform", + "plating", + "platinum", + "platonic", + "platter", + "platypus", + "plausible", + "plausibly", + "playable", + "playback", + "player", + "playful", + "playgroup", + "playhouse", + "playing", + "playlist", + "playmaker", + "playmate", + "playoff", + "playpen", + "playroom", + "playset", + "plaything", + "playtime", + "plaza", + "pleading", + "pleat", + "pledge", + "plentiful", + "plenty", + "plethora", + "plexiglas", + "pliable", + "plod", + "plop", + "plot", + "plow", + "ploy", + "pluck", + "plug", + "plunder", + "plunging", + "plural", + "plus", + "plutonium", + "plywood", + "poach", + "pod", + "poem", + "poet", + "pogo", + "pointed", + "pointer", + "pointing", + "pointless", + "pointy", + "poise", + "poison", + "poker", + "poking", + "polar", + "police", + "policy", + "polio", + "polish", + "politely", + "polka", + "polo", + "polyester", + "polygon", + "polygraph", + "polymer", + "poncho", + "pond", + "pony", + "popcorn", + "pope", + "poplar", + "popper", + "poppy", + "popsicle", + "populace", + "popular", + "populate", + "porcupine", + "pork", + "porous", + "porridge", + "portable", + "portal", + "portfolio", + "porthole", + "portion", + "portly", + "portside", + "poser", + "posh", + "posing", + "possible", + "possibly", + "possum", + "postage", + "postal", + "postbox", + "postcard", + "posted", + "poster", + "posting", + "postnasal", + "posture", + "postwar", + "pouch", + "pounce", + "pouncing", + "pound", + "pouring", + "pout", + "powdered", + "powdering", + "powdery", + "power", + "powwow", + "pox", + "praising", + "prance", + "prancing", + "pranker", + "prankish", + "prankster", + "prayer", + "praying", + "preacher", + "preaching", + "preachy", + "preamble", + "precinct", + "precise", + "precision", + "precook", + "precut", + "predator", + "predefine", + "predict", + "preface", + "prefix", + "preflight", + "preformed", + "pregame", + "pregnancy", + "pregnant", + "preheated", + "prelaunch", + "prelaw", + "prelude", + "premiere", + "premises", + "premium", + "prenatal", + "preoccupy", + "preorder", + "prepaid", + "prepay", + "preplan", + "preppy", + "preschool", + "prescribe", + "preseason", + "preset", + "preshow", + "president", + "presoak", + "press", + "presume", + "presuming", + "preteen", + "pretended", + "pretender", + "pretense", + "pretext", + "pretty", + "pretzel", + "prevail", + "prevalent", + "prevent", + "preview", + "previous", + "prewar", + "prewashed", + "prideful", + "pried", + "primal", + "primarily", + "primary", + "primate", + "primer", + "primp", + "princess", + "print", + "prior", + "prism", + "prison", + "prissy", + "pristine", + "privacy", + "private", + "privatize", + "prize", + "proactive", + "probable", + "probably", + "probation", + "probe", + "probing", + "probiotic", + "problem", + "procedure", + "process", + "proclaim", + "procreate", + "procurer", + "prodigal", + "prodigy", + "produce", + "product", + "profane", + "profanity", + "professed", + "professor", + "profile", + "profound", + "profusely", + "progeny", + "prognosis", + "program", + "progress", + "projector", + "prologue", + "prolonged", + "promenade", + "prominent", + "promoter", + "promotion", + "prompter", + "promptly", + "prone", + "prong", + "pronounce", + "pronto", + "proofing", + "proofread", + "proofs", + "propeller", + "properly", + "property", + "proponent", + "proposal", + "propose", + "props", + "prorate", + "protector", + "protegee", + "proton", + "prototype", + "protozoan", + "protract", + "protrude", + "proud", + "provable", + "proved", + "proven", + "provided", + "provider", + "providing", + "province", + "proving", + "provoke", + "provoking", + "provolone", + "prowess", + "prowler", + "prowling", + "proximity", + "proxy", + "prozac", + "prude", + "prudishly", + "prune", + "pruning", + "pry", + "psychic", + "public", + "publisher", + "pucker", + "pueblo", + "pug", + "pull", + "pulmonary", + "pulp", + "pulsate", + "pulse", + "pulverize", + "puma", + "pumice", + "pummel", + "punch", + "punctual", + "punctuate", + "punctured", + "pungent", + "punisher", + "punk", + "pupil", + "puppet", + "puppy", + "purchase", + "pureblood", + "purebred", + "purely", + "pureness", + "purgatory", + "purge", + "purging", + "purifier", + "purify", + "purist", + "puritan", + "purity", + "purple", + "purplish", + "purposely", + "purr", + "purse", + "pursuable", + "pursuant", + "pursuit", + "purveyor", + "pushcart", + "pushchair", + "pusher", + "pushiness", + "pushing", + "pushover", + "pushpin", + "pushup", + "pushy", + "putdown", + "putt", + "puzzle", + "puzzling", + "pyramid", + "pyromania", + "python", + "quack", + "quadrant", + "quail", + "quaintly", + "quake", + "quaking", + "qualified", + "qualifier", + "qualify", + "quality", + "qualm", + "quantum", + "quarrel", + "quarry", + "quartered", + "quarterly", + "quarters", + "quartet", + "quench", + "query", + "quicken", + "quickly", + "quickness", + "quicksand", + "quickstep", + "quiet", + "quill", + "quilt", + "quintet", + "quintuple", + "quirk", + "quit", + "quiver", + "quizzical", + "quotable", + "quotation", + "quote", + "rabid", + "race", + "racing", + "racism", + "rack", + "racoon", + "radar", + "radial", + "radiance", + "radiantly", + "radiated", + "radiation", + "radiator", + "radio", + "radish", + "raffle", + "raft", + "rage", + "ragged", + "raging", + "ragweed", + "raider", + "railcar", + "railing", + "railroad", + "railway", + "raisin", + "rake", + "raking", + "rally", + "ramble", + "rambling", + "ramp", + "ramrod", + "ranch", + "rancidity", + "random", + "ranged", + "ranger", + "ranging", + "ranked", + "ranking", + "ransack", + "ranting", + "rants", + "rare", + "rarity", + "rascal", + "rash", + "rasping", + "ravage", + "raven", + "ravine", + "raving", + "ravioli", + "ravishing", + "reabsorb", + "reach", + "reacquire", + "reaction", + "reactive", + "reactor", + "reaffirm", + "ream", + "reanalyze", + "reappear", + "reapply", + "reappoint", + "reapprove", + "rearrange", + "rearview", + "reason", + "reassign", + "reassure", + "reattach", + "reawake", + "rebalance", + "rebate", + "rebel", + "rebirth", + "reboot", + "reborn", + "rebound", + "rebuff", + "rebuild", + "rebuilt", + "reburial", + "rebuttal", + "recall", + "recant", + "recapture", + "recast", + "recede", + "recent", + "recess", + "recharger", + "recipient", + "recital", + "recite", + "reckless", + "reclaim", + "recliner", + "reclining", + "recluse", + "reclusive", + "recognize", + "recoil", + "recollect", + "recolor", + "reconcile", + "reconfirm", + "reconvene", + "recopy", + "record", + "recount", + "recoup", + "recovery", + "recreate", + "rectal", + "rectangle", + "rectified", + "rectify", + "recycled", + "recycler", + "recycling", + "reemerge", + "reenact", + "reenter", + "reentry", + "reexamine", + "referable", + "referee", + "reference", + "refill", + "refinance", + "refined", + "refinery", + "refining", + "refinish", + "reflected", + "reflector", + "reflex", + "reflux", + "refocus", + "refold", + "reforest", + "reformat", + "reformed", + "reformer", + "reformist", + "refract", + "refrain", + "refreeze", + "refresh", + "refried", + "refueling", + "refund", + "refurbish", + "refurnish", + "refusal", + "refuse", + "refusing", + "refutable", + "refute", + "regain", + "regalia", + "regally", + "reggae", + "regime", + "region", + "register", + "registrar", + "registry", + "regress", + "regretful", + "regroup", + "regular", + "regulate", + "regulator", + "rehab", + "reheat", + "rehire", + "rehydrate", + "reimburse", + "reissue", + "reiterate", + "rejoice", + "rejoicing", + "rejoin", + "rekindle", + "relapse", + "relapsing", + "relatable", + "related", + "relation", + "relative", + "relax", + "relay", + "relearn", + "release", + "relenting", + "reliable", + "reliably", + "reliance", + "reliant", + "relic", + "relieve", + "relieving", + "relight", + "relish", + "relive", + "reload", + "relocate", + "relock", + "reluctant", + "rely", + "remake", + "remark", + "remarry", + "rematch", + "remedial", + "remedy", + "remember", + "reminder", + "remindful", + "remission", + "remix", + "remnant", + "remodeler", + "remold", + "remorse", + "remote", + "removable", + "removal", + "removed", + "remover", + "removing", + "rename", + "renderer", + "rendering", + "rendition", + "renegade", + "renewable", + "renewably", + "renewal", + "renewed", + "renounce", + "renovate", + "renovator", + "rentable", + "rental", + "rented", + "renter", + "reoccupy", + "reoccur", + "reopen", + "reorder", + "repackage", + "repacking", + "repaint", + "repair", + "repave", + "repaying", + "repayment", + "repeal", + "repeated", + "repeater", + "repent", + "rephrase", + "replace", + "replay", + "replica", + "reply", + "reporter", + "repose", + "repossess", + "repost", + "repressed", + "reprimand", + "reprint", + "reprise", + "reproach", + "reprocess", + "reproduce", + "reprogram", + "reps", + "reptile", + "reptilian", + "repugnant", + "repulsion", + "repulsive", + "repurpose", + "reputable", + "reputably", + "request", + "require", + "requisite", + "reroute", + "rerun", + "resale", + "resample", + "rescuer", + "reseal", + "research", + "reselect", + "reseller", + "resemble", + "resend", + "resent", + "reset", + "reshape", + "reshoot", + "reshuffle", + "residence", + "residency", + "resident", + "residual", + "residue", + "resigned", + "resilient", + "resistant", + "resisting", + "resize", + "resolute", + "resolved", + "resonant", + "resonate", + "resort", + "resource", + "respect", + "resubmit", + "result", + "resume", + "resupply", + "resurface", + "resurrect", + "retail", + "retainer", + "retaining", + "retake", + "retaliate", + "retention", + "rethink", + "retinal", + "retired", + "retiree", + "retiring", + "retold", + "retool", + "retorted", + "retouch", + "retrace", + "retract", + "retrain", + "retread", + "retreat", + "retrial", + "retrieval", + "retriever", + "retry", + "return", + "retying", + "retype", + "reunion", + "reunite", + "reusable", + "reuse", + "reveal", + "reveler", + "revenge", + "revenue", + "reverb", + "revered", + "reverence", + "reverend", + "reversal", + "reverse", + "reversing", + "reversion", + "revert", + "revisable", + "revise", + "revision", + "revisit", + "revivable", + "revival", + "reviver", + "reviving", + "revocable", + "revoke", + "revolt", + "revolver", + "revolving", + "reward", + "rewash", + "rewind", + "rewire", + "reword", + "rework", + "rewrap", + "rewrite", + "rhyme", + "ribbon", + "ribcage", + "rice", + "riches", + "richly", + "richness", + "rickety", + "ricotta", + "riddance", + "ridden", + "ride", + "riding", + "rifling", + "rift", + "rigging", + "rigid", + "rigor", + "rimless", + "rimmed", + "rind", + "rink", + "rinse", + "rinsing", + "riot", + "ripcord", + "ripeness", + "ripening", + "ripping", + "ripple", + "rippling", + "riptide", + "rise", + "rising", + "risk", + "risotto", + "ritalin", + "ritzy", + "rival", + "riverbank", + "riverbed", + "riverboat", + "riverside", + "riveter", + "riveting", + "roamer", + "roaming", + "roast", + "robbing", + "robe", + "robin", + "robotics", + "robust", + "rockband", + "rocker", + "rocket", + "rockfish", + "rockiness", + "rocking", + "rocklike", + "rockslide", + "rockstar", + "rocky", + "rogue", + "roman", + "romp", + "rope", + "roping", + "roster", + "rosy", + "rotten", + "rotting", + "rotunda", + "roulette", + "rounding", + "roundish", + "roundness", + "roundup", + "roundworm", + "routine", + "routing", + "rover", + "roving", + "royal", + "rubbed", + "rubber", + "rubbing", + "rubble", + "rubdown", + "ruby", + "ruckus", + "rudder", + "rug", + "ruined", + "rule", + "rumble", + "rumbling", + "rummage", + "rumor", + "runaround", + "rundown", + "runner", + "running", + "runny", + "runt", + "runway", + "rupture", + "rural", + "ruse", + "rush", + "rust", + "rut", + "sabbath", + "sabotage", + "sacrament", + "sacred", + "sacrifice", + "sadden", + "saddlebag", + "saddled", + "saddling", + "sadly", + "sadness", + "safari", + "safeguard", + "safehouse", + "safely", + "safeness", + "saffron", + "saga", + "sage", + "sagging", + "saggy", + "said", + "saint", + "sake", + "salad", + "salami", + "salaried", + "salary", + "saline", + "salon", + "saloon", + "salsa", + "salt", + "salutary", + "salute", + "salvage", + "salvaging", + "salvation", + "same", + "sample", + "sampling", + "sanction", + "sanctity", + "sanctuary", + "sandal", + "sandbag", + "sandbank", + "sandbar", + "sandblast", + "sandbox", + "sanded", + "sandfish", + "sanding", + "sandlot", + "sandpaper", + "sandpit", + "sandstone", + "sandstorm", + "sandworm", + "sandy", + "sanitary", + "sanitizer", + "sank", + "santa", + "sapling", + "sappiness", + "sappy", + "sarcasm", + "sarcastic", + "sardine", + "sash", + "sasquatch", + "sassy", + "satchel", + "satiable", + "satin", + "satirical", + "satisfied", + "satisfy", + "saturate", + "saturday", + "sauciness", + "saucy", + "sauna", + "savage", + "savanna", + "saved", + "savings", + "savior", + "savor", + "saxophone", + "say", + "scabbed", + "scabby", + "scalded", + "scalding", + "scale", + "scaling", + "scallion", + "scallop", + "scalping", + "scam", + "scandal", + "scanner", + "scanning", + "scant", + "scapegoat", + "scarce", + "scarcity", + "scarecrow", + "scared", + "scarf", + "scarily", + "scariness", + "scarring", + "scary", + "scavenger", + "scenic", + "schedule", + "schematic", + "scheme", + "scheming", + "schilling", + "schnapps", + "scholar", + "science", + "scientist", + "scion", + "scoff", + "scolding", + "scone", + "scoop", + "scooter", + "scope", + "scorch", + "scorebook", + "scorecard", + "scored", + "scoreless", + "scorer", + "scoring", + "scorn", + "scorpion", + "scotch", + "scoundrel", + "scoured", + "scouring", + "scouting", + "scouts", + "scowling", + "scrabble", + "scraggly", + "scrambled", + "scrambler", + "scrap", + "scratch", + "scrawny", + "screen", + "scribble", + "scribe", + "scribing", + "scrimmage", + "script", + "scroll", + "scrooge", + "scrounger", + "scrubbed", + "scrubber", + "scruffy", + "scrunch", + "scrutiny", + "scuba", + "scuff", + "sculptor", + "sculpture", + "scurvy", + "scuttle", + "secluded", + "secluding", + "seclusion", + "second", + "secrecy", + "secret", + "sectional", + "sector", + "secular", + "securely", + "security", + "sedan", + "sedate", + "sedation", + "sedative", + "sediment", + "seduce", + "seducing", + "segment", + "seismic", + "seizing", + "seldom", + "selected", + "selection", + "selective", + "selector", + "self", + "seltzer", + "semantic", + "semester", + "semicolon", + "semifinal", + "seminar", + "semisoft", + "semisweet", + "senate", + "senator", + "send", + "senior", + "senorita", + "sensation", + "sensitive", + "sensitize", + "sensually", + "sensuous", + "sepia", + "september", + "septic", + "septum", + "sequel", + "sequence", + "sequester", + "series", + "sermon", + "serotonin", + "serpent", + "serrated", + "serve", + "service", + "serving", + "sesame", + "sessions", + "setback", + "setting", + "settle", + "settling", + "setup", + "sevenfold", + "seventeen", + "seventh", + "seventy", + "severity", + "shabby", + "shack", + "shaded", + "shadily", + "shadiness", + "shading", + "shadow", + "shady", + "shaft", + "shakable", + "shakily", + "shakiness", + "shaking", + "shaky", + "shale", + "shallot", + "shallow", + "shame", + "shampoo", + "shamrock", + "shank", + "shanty", + "shape", + "shaping", + "share", + "sharpener", + "sharper", + "sharpie", + "sharply", + "sharpness", + "shawl", + "sheath", + "shed", + "sheep", + "sheet", + "shelf", + "shell", + "shelter", + "shelve", + "shelving", + "sherry", + "shield", + "shifter", + "shifting", + "shiftless", + "shifty", + "shimmer", + "shimmy", + "shindig", + "shine", + "shingle", + "shininess", + "shining", + "shiny", + "ship", + "shirt", + "shivering", + "shock", + "shone", + "shoplift", + "shopper", + "shopping", + "shoptalk", + "shore", + "shortage", + "shortcake", + "shortcut", + "shorten", + "shorter", + "shorthand", + "shortlist", + "shortly", + "shortness", + "shorts", + "shortwave", + "shorty", + "shout", + "shove", + "showbiz", + "showcase", + "showdown", + "shower", + "showgirl", + "showing", + "showman", + "shown", + "showoff", + "showpiece", + "showplace", + "showroom", + "showy", + "shrank", + "shrapnel", + "shredder", + "shredding", + "shrewdly", + "shriek", + "shrill", + "shrimp", + "shrine", + "shrink", + "shrivel", + "shrouded", + "shrubbery", + "shrubs", + "shrug", + "shrunk", + "shucking", + "shudder", + "shuffle", + "shuffling", + "shun", + "shush", + "shut", + "shy", + "siamese", + "siberian", + "sibling", + "siding", + "sierra", + "siesta", + "sift", + "sighing", + "silenced", + "silencer", + "silent", + "silica", + "silicon", + "silk", + "silliness", + "silly", + "silo", + "silt", + "silver", + "similarly", + "simile", + "simmering", + "simple", + "simplify", + "simply", + "sincere", + "sincerity", + "singer", + "singing", + "single", + "singular", + "sinister", + "sinless", + "sinner", + "sinuous", + "sip", + "siren", + "sister", + "sitcom", + "sitter", + "sitting", + "situated", + "situation", + "sixfold", + "sixteen", + "sixth", + "sixties", + "sixtieth", + "sixtyfold", + "sizable", + "sizably", + "size", + "sizing", + "sizzle", + "sizzling", + "skater", + "skating", + "skedaddle", + "skeletal", + "skeleton", + "skeptic", + "sketch", + "skewed", + "skewer", + "skid", + "skied", + "skier", + "skies", + "skiing", + "skilled", + "skillet", + "skillful", + "skimmed", + "skimmer", + "skimming", + "skimpily", + "skincare", + "skinhead", + "skinless", + "skinning", + "skinny", + "skintight", + "skipper", + "skipping", + "skirmish", + "skirt", + "skittle", + "skydiver", + "skylight", + "skyline", + "skype", + "skyrocket", + "skyward", + "slab", + "slacked", + "slacker", + "slacking", + "slackness", + "slacks", + "slain", + "slam", + "slander", + "slang", + "slapping", + "slapstick", + "slashed", + "slashing", + "slate", + "slather", + "slaw", + "sled", + "sleek", + "sleep", + "sleet", + "sleeve", + "slept", + "sliceable", + "sliced", + "slicer", + "slicing", + "slick", + "slider", + "slideshow", + "sliding", + "slighted", + "slighting", + "slightly", + "slimness", + "slimy", + "slinging", + "slingshot", + "slinky", + "slip", + "slit", + "sliver", + "slobbery", + "slogan", + "sloped", + "sloping", + "sloppily", + "sloppy", + "slot", + "slouching", + "slouchy", + "sludge", + "slug", + "slum", + "slurp", + "slush", + "sly", + "small", + "smartly", + "smartness", + "smasher", + "smashing", + "smashup", + "smell", + "smelting", + "smile", + "smilingly", + "smirk", + "smite", + "smith", + "smitten", + "smock", + "smog", + "smoked", + "smokeless", + "smokiness", + "smoking", + "smoky", + "smolder", + "smooth", + "smother", + "smudge", + "smudgy", + "smuggler", + "smuggling", + "smugly", + "smugness", + "snack", + "snagged", + "snaking", + "snap", + "snare", + "snarl", + "snazzy", + "sneak", + "sneer", + "sneeze", + "sneezing", + "snide", + "sniff", + "snippet", + "snipping", + "snitch", + "snooper", + "snooze", + "snore", + "snoring", + "snorkel", + "snort", + "snout", + "snowbird", + "snowboard", + "snowbound", + "snowcap", + "snowdrift", + "snowdrop", + "snowfall", + "snowfield", + "snowflake", + "snowiness", + "snowless", + "snowman", + "snowplow", + "snowshoe", + "snowstorm", + "snowsuit", + "snowy", + "snub", + "snuff", + "snuggle", + "snugly", + "snugness", + "speak", + "spearfish", + "spearhead", + "spearman", + "spearmint", + "species", + "specimen", + "specked", + "speckled", + "specks", + "spectacle", + "spectator", + "spectrum", + "speculate", + "speech", + "speed", + "spellbind", + "speller", + "spelling", + "spendable", + "spender", + "spending", + "spent", + "spew", + "sphere", + "spherical", + "sphinx", + "spider", + "spied", + "spiffy", + "spill", + "spilt", + "spinach", + "spinal", + "spindle", + "spinner", + "spinning", + "spinout", + "spinster", + "spiny", + "spiral", + "spirited", + "spiritism", + "spirits", + "spiritual", + "splashed", + "splashing", + "splashy", + "splatter", + "spleen", + "splendid", + "splendor", + "splice", + "splicing", + "splinter", + "splotchy", + "splurge", + "spoilage", + "spoiled", + "spoiler", + "spoiling", + "spoils", + "spoken", + "spokesman", + "sponge", + "spongy", + "sponsor", + "spoof", + "spookily", + "spooky", + "spool", + "spoon", + "spore", + "sporting", + "sports", + "sporty", + "spotless", + "spotlight", + "spotted", + "spotter", + "spotting", + "spotty", + "spousal", + "spouse", + "spout", + "sprain", + "sprang", + "sprawl", + "spray", + "spree", + "sprig", + "spring", + "sprinkled", + "sprinkler", + "sprint", + "sprite", + "sprout", + "spruce", + "sprung", + "spry", + "spud", + "spur", + "sputter", + "spyglass", + "squabble", + "squad", + "squall", + "squander", + "squash", + "squatted", + "squatter", + "squatting", + "squeak", + "squealer", + "squealing", + "squeamish", + "squeegee", + "squeeze", + "squeezing", + "squid", + "squiggle", + "squiggly", + "squint", + "squire", + "squirt", + "squishier", + "squishy", + "stability", + "stabilize", + "stable", + "stack", + "stadium", + "staff", + "stage", + "staging", + "stagnant", + "stagnate", + "stainable", + "stained", + "staining", + "stainless", + "stalemate", + "staleness", + "stalling", + "stallion", + "stamina", + "stammer", + "stamp", + "stand", + "stank", + "staple", + "stapling", + "starboard", + "starch", + "stardom", + "stardust", + "starfish", + "stargazer", + "staring", + "stark", + "starless", + "starlet", + "starlight", + "starlit", + "starring", + "starry", + "starship", + "starter", + "starting", + "startle", + "startling", + "startup", + "starved", + "starving", + "stash", + "state", + "static", + "statistic", + "statue", + "stature", + "status", + "statute", + "statutory", + "staunch", + "stays", + "steadfast", + "steadier", + "steadily", + "steadying", + "steam", + "steed", + "steep", + "steerable", + "steering", + "steersman", + "stegosaur", + "stellar", + "stem", + "stench", + "stencil", + "step", + "stereo", + "sterile", + "sterility", + "sterilize", + "sterling", + "sternness", + "sternum", + "stew", + "stick", + "stiffen", + "stiffly", + "stiffness", + "stifle", + "stifling", + "stillness", + "stilt", + "stimulant", + "stimulate", + "stimuli", + "stimulus", + "stinger", + "stingily", + "stinging", + "stingray", + "stingy", + "stinking", + "stinky", + "stipend", + "stipulate", + "stir", + "stitch", + "stock", + "stoic", + "stoke", + "stole", + "stomp", + "stonewall", + "stoneware", + "stonework", + "stoning", + "stony", + "stood", + "stooge", + "stool", + "stoop", + "stoplight", + "stoppable", + "stoppage", + "stopped", + "stopper", + "stopping", + "stopwatch", + "storable", + "storage", + "storeroom", + "storewide", + "storm", + "stout", + "stove", + "stowaway", + "stowing", + "straddle", + "straggler", + "strained", + "strainer", + "straining", + "strangely", + "stranger", + "strangle", + "strategic", + "strategy", + "stratus", + "straw", + "stray", + "streak", + "stream", + "street", + "strength", + "strenuous", + "strep", + "stress", + "stretch", + "strewn", + "stricken", + "strict", + "stride", + "strife", + "strike", + "striking", + "strive", + "striving", + "strobe", + "strode", + "stroller", + "strongbox", + "strongly", + "strongman", + "struck", + "structure", + "strudel", + "struggle", + "strum", + "strung", + "strut", + "stubbed", + "stubble", + "stubbly", + "stubborn", + "stucco", + "stuck", + "student", + "studied", + "studio", + "study", + "stuffed", + "stuffing", + "stuffy", + "stumble", + "stumbling", + "stump", + "stung", + "stunned", + "stunner", + "stunning", + "stunt", + "stupor", + "sturdily", + "sturdy", + "styling", + "stylishly", + "stylist", + "stylized", + "stylus", + "suave", + "subarctic", + "subatomic", + "subdivide", + "subdued", + "subduing", + "subfloor", + "subgroup", + "subheader", + "subject", + "sublease", + "sublet", + "sublevel", + "sublime", + "submarine", + "submerge", + "submersed", + "submitter", + "subpanel", + "subpar", + "subplot", + "subprime", + "subscribe", + "subscript", + "subsector", + "subside", + "subsiding", + "subsidize", + "subsidy", + "subsoil", + "subsonic", + "substance", + "subsystem", + "subtext", + "subtitle", + "subtly", + "subtotal", + "subtract", + "subtype", + "suburb", + "subway", + "subwoofer", + "subzero", + "succulent", + "such", + "suction", + "sudden", + "sudoku", + "suds", + "sufferer", + "suffering", + "suffice", + "suffix", + "suffocate", + "suffrage", + "sugar", + "suggest", + "suing", + "suitable", + "suitably", + "suitcase", + "suitor", + "sulfate", + "sulfide", + "sulfite", + "sulfur", + "sulk", + "sullen", + "sulphate", + "sulphuric", + "sultry", + "superbowl", + "superglue", + "superhero", + "superior", + "superjet", + "superman", + "supermom", + "supernova", + "supervise", + "supper", + "supplier", + "supply", + "support", + "supremacy", + "supreme", + "surcharge", + "surely", + "sureness", + "surface", + "surfacing", + "surfboard", + "surfer", + "surgery", + "surgical", + "surging", + "surname", + "surpass", + "surplus", + "surprise", + "surreal", + "surrender", + "surrogate", + "surround", + "survey", + "survival", + "survive", + "surviving", + "survivor", + "sushi", + "suspect", + "suspend", + "suspense", + "sustained", + "sustainer", + "swab", + "swaddling", + "swagger", + "swampland", + "swan", + "swapping", + "swarm", + "sway", + "swear", + "sweat", + "sweep", + "swell", + "swept", + "swerve", + "swifter", + "swiftly", + "swiftness", + "swimmable", + "swimmer", + "swimming", + "swimsuit", + "swimwear", + "swinger", + "swinging", + "swipe", + "swirl", + "switch", + "swivel", + "swizzle", + "swooned", + "swoop", + "swoosh", + "swore", + "sworn", + "swung", + "sycamore", + "sympathy", + "symphonic", + "symphony", + "symptom", + "synapse", + "syndrome", + "synergy", + "synopses", + "synopsis", + "synthesis", + "synthetic", + "syrup", + "system", + "t-shirt", + "tabasco", + "tabby", + "tableful", + "tables", + "tablet", + "tableware", + "tabloid", + "tackiness", + "tacking", + "tackle", + "tackling", + "tacky", + "taco", + "tactful", + "tactical", + "tactics", + "tactile", + "tactless", + "tadpole", + "taekwondo", + "tag", + "tainted", + "take", + "taking", + "talcum", + "talisman", + "tall", + "talon", + "tamale", + "tameness", + "tamer", + "tamper", + "tank", + "tanned", + "tannery", + "tanning", + "tantrum", + "tapeless", + "tapered", + "tapering", + "tapestry", + "tapioca", + "tapping", + "taps", + "tarantula", + "target", + "tarmac", + "tarnish", + "tarot", + "tartar", + "tartly", + "tartness", + "task", + "tassel", + "taste", + "tastiness", + "tasting", + "tasty", + "tattered", + "tattle", + "tattling", + "tattoo", + "taunt", + "tavern", + "thank", + "that", + "thaw", + "theater", + "theatrics", + "thee", + "theft", + "theme", + "theology", + "theorize", + "thermal", + "thermos", + "thesaurus", + "these", + "thesis", + "thespian", + "thicken", + "thicket", + "thickness", + "thieving", + "thievish", + "thigh", + "thimble", + "thing", + "think", + "thinly", + "thinner", + "thinness", + "thinning", + "thirstily", + "thirsting", + "thirsty", + "thirteen", + "thirty", + "thong", + "thorn", + "those", + "thousand", + "thrash", + "thread", + "threaten", + "threefold", + "thrift", + "thrill", + "thrive", + "thriving", + "throat", + "throbbing", + "throng", + "throttle", + "throwaway", + "throwback", + "thrower", + "throwing", + "thud", + "thumb", + "thumping", + "thursday", + "thus", + "thwarting", + "thyself", + "tiara", + "tibia", + "tidal", + "tidbit", + "tidiness", + "tidings", + "tidy", + "tiger", + "tighten", + "tightly", + "tightness", + "tightrope", + "tightwad", + "tigress", + "tile", + "tiling", + "till", + "tilt", + "timid", + "timing", + "timothy", + "tinderbox", + "tinfoil", + "tingle", + "tingling", + "tingly", + "tinker", + "tinkling", + "tinsel", + "tinsmith", + "tint", + "tinwork", + "tiny", + "tipoff", + "tipped", + "tipper", + "tipping", + "tiptoeing", + "tiptop", + "tiring", + "tissue", + "trace", + "tracing", + "track", + "traction", + "tractor", + "trade", + "trading", + "tradition", + "traffic", + "tragedy", + "trailing", + "trailside", + "train", + "traitor", + "trance", + "tranquil", + "transfer", + "transform", + "translate", + "transpire", + "transport", + "transpose", + "trapdoor", + "trapeze", + "trapezoid", + "trapped", + "trapper", + "trapping", + "traps", + "trash", + "travel", + "traverse", + "travesty", + "tray", + "treachery", + "treading", + "treadmill", + "treason", + "treat", + "treble", + "tree", + "trekker", + "tremble", + "trembling", + "tremor", + "trench", + "trend", + "trespass", + "triage", + "trial", + "triangle", + "tribesman", + "tribunal", + "tribune", + "tributary", + "tribute", + "triceps", + "trickery", + "trickily", + "tricking", + "trickle", + "trickster", + "tricky", + "tricolor", + "tricycle", + "trident", + "tried", + "trifle", + "trifocals", + "trillion", + "trilogy", + "trimester", + "trimmer", + "trimming", + "trimness", + "trinity", + "trio", + "tripod", + "tripping", + "triumph", + "trivial", + "trodden", + "trolling", + "trombone", + "trophy", + "tropical", + "tropics", + "trouble", + "troubling", + "trough", + "trousers", + "trout", + "trowel", + "truce", + "truck", + "truffle", + "trump", + "trunks", + "trustable", + "trustee", + "trustful", + "trusting", + "trustless", + "truth", + "try", + "tubby", + "tubeless", + "tubular", + "tucking", + "tuesday", + "tug", + "tuition", + "tulip", + "tumble", + "tumbling", + "tummy", + "turban", + "turbine", + "turbofan", + "turbojet", + "turbulent", + "turf", + "turkey", + "turmoil", + "turret", + "turtle", + "tusk", + "tutor", + "tutu", + "tux", + "tweak", + "tweed", + "tweet", + "tweezers", + "twelve", + "twentieth", + "twenty", + "twerp", + "twice", + "twiddle", + "twiddling", + "twig", + "twilight", + "twine", + "twins", + "twirl", + "twistable", + "twisted", + "twister", + "twisting", + "twisty", + "twitch", + "twitter", + "tycoon", + "tying", + "tyke", + "udder", + "ultimate", + "ultimatum", + "ultra", + "umbilical", + "umbrella", + "umpire", + "unabashed", + "unable", + "unadorned", + "unadvised", + "unafraid", + "unaired", + "unaligned", + "unaltered", + "unarmored", + "unashamed", + "unaudited", + "unawake", + "unaware", + "unbaked", + "unbalance", + "unbeaten", + "unbend", + "unbent", + "unbiased", + "unbitten", + "unblended", + "unblessed", + "unblock", + "unbolted", + "unbounded", + "unboxed", + "unbraided", + "unbridle", + "unbroken", + "unbuckled", + "unbundle", + "unburned", + "unbutton", + "uncanny", + "uncapped", + "uncaring", + "uncertain", + "unchain", + "unchanged", + "uncharted", + "uncheck", + "uncivil", + "unclad", + "unclaimed", + "unclamped", + "unclasp", + "uncle", + "unclip", + "uncloak", + "unclog", + "unclothed", + "uncoated", + "uncoiled", + "uncolored", + "uncombed", + "uncommon", + "uncooked", + "uncork", + "uncorrupt", + "uncounted", + "uncouple", + "uncouth", + "uncover", + "uncross", + "uncrown", + "uncrushed", + "uncured", + "uncurious", + "uncurled", + "uncut", + "undamaged", + "undated", + "undaunted", + "undead", + "undecided", + "undefined", + "underage", + "underarm", + "undercoat", + "undercook", + "undercut", + "underdog", + "underdone", + "underfed", + "underfeed", + "underfoot", + "undergo", + "undergrad", + "underhand", + "underline", + "underling", + "undermine", + "undermost", + "underpaid", + "underpass", + "underpay", + "underrate", + "undertake", + "undertone", + "undertook", + "undertow", + "underuse", + "underwear", + "underwent", + "underwire", + "undesired", + "undiluted", + "undivided", + "undocked", + "undoing", + "undone", + "undrafted", + "undress", + "undrilled", + "undusted", + "undying", + "unearned", + "unearth", + "unease", + "uneasily", + "uneasy", + "uneatable", + "uneaten", + "unedited", + "unelected", + "unending", + "unengaged", + "unenvied", + "unequal", + "unethical", + "uneven", + "unexpired", + "unexposed", + "unfailing", + "unfair", + "unfasten", + "unfazed", + "unfeeling", + "unfiled", + "unfilled", + "unfitted", + "unfitting", + "unfixable", + "unfixed", + "unflawed", + "unfocused", + "unfold", + "unfounded", + "unframed", + "unfreeze", + "unfrosted", + "unfrozen", + "unfunded", + "unglazed", + "ungloved", + "unglue", + "ungodly", + "ungraded", + "ungreased", + "unguarded", + "unguided", + "unhappily", + "unhappy", + "unharmed", + "unhealthy", + "unheard", + "unhearing", + "unheated", + "unhelpful", + "unhidden", + "unhinge", + "unhitched", + "unholy", + "unhook", + "unicorn", + "unicycle", + "unified", + "unifier", + "uniformed", + "uniformly", + "unify", + "unimpeded", + "uninjured", + "uninstall", + "uninsured", + "uninvited", + "union", + "uniquely", + "unisexual", + "unison", + "unissued", + "unit", + "universal", + "universe", + "unjustly", + "unkempt", + "unkind", + "unknotted", + "unknowing", + "unknown", + "unlaced", + "unlatch", + "unlawful", + "unleaded", + "unlearned", + "unleash", + "unless", + "unleveled", + "unlighted", + "unlikable", + "unlimited", + "unlined", + "unlinked", + "unlisted", + "unlit", + "unlivable", + "unloaded", + "unloader", + "unlocked", + "unlocking", + "unlovable", + "unloved", + "unlovely", + "unloving", + "unluckily", + "unlucky", + "unmade", + "unmanaged", + "unmanned", + "unmapped", + "unmarked", + "unmasked", + "unmasking", + "unmatched", + "unmindful", + "unmixable", + "unmixed", + "unmolded", + "unmoral", + "unmovable", + "unmoved", + "unmoving", + "unnamable", + "unnamed", + "unnatural", + "unneeded", + "unnerve", + "unnerving", + "unnoticed", + "unopened", + "unopposed", + "unpack", + "unpadded", + "unpaid", + "unpainted", + "unpaired", + "unpaved", + "unpeeled", + "unpicked", + "unpiloted", + "unpinned", + "unplanned", + "unplanted", + "unpleased", + "unpledged", + "unplowed", + "unplug", + "unpopular", + "unproven", + "unquote", + "unranked", + "unrated", + "unraveled", + "unreached", + "unread", + "unreal", + "unreeling", + "unrefined", + "unrelated", + "unrented", + "unrest", + "unretired", + "unrevised", + "unrigged", + "unripe", + "unrivaled", + "unroasted", + "unrobed", + "unroll", + "unruffled", + "unruly", + "unrushed", + "unsaddle", + "unsafe", + "unsaid", + "unsalted", + "unsaved", + "unsavory", + "unscathed", + "unscented", + "unscrew", + "unsealed", + "unseated", + "unsecured", + "unseeing", + "unseemly", + "unseen", + "unselect", + "unselfish", + "unsent", + "unsettled", + "unshackle", + "unshaken", + "unshaved", + "unshaven", + "unsheathe", + "unshipped", + "unsightly", + "unsigned", + "unskilled", + "unsliced", + "unsmooth", + "unsnap", + "unsocial", + "unsoiled", + "unsold", + "unsolved", + "unsorted", + "unspoiled", + "unspoken", + "unstable", + "unstaffed", + "unstamped", + "unsteady", + "unsterile", + "unstirred", + "unstitch", + "unstopped", + "unstuck", + "unstuffed", + "unstylish", + "unsubtle", + "unsubtly", + "unsuited", + "unsure", + "unsworn", + "untagged", + "untainted", + "untaken", + "untamed", + "untangled", + "untapped", + "untaxed", + "unthawed", + "unthread", + "untidy", + "untie", + "until", + "untimed", + "untimely", + "untitled", + "untoasted", + "untold", + "untouched", + "untracked", + "untrained", + "untreated", + "untried", + "untrimmed", + "untrue", + "untruth", + "unturned", + "untwist", + "untying", + "unusable", + "unused", + "unusual", + "unvalued", + "unvaried", + "unvarying", + "unveiled", + "unveiling", + "unvented", + "unviable", + "unvisited", + "unvocal", + "unwanted", + "unwarlike", + "unwary", + "unwashed", + "unwatched", + "unweave", + "unwed", + "unwelcome", + "unwell", + "unwieldy", + "unwilling", + "unwind", + "unwired", + "unwitting", + "unwomanly", + "unworldly", + "unworn", + "unworried", + "unworthy", + "unwound", + "unwoven", + "unwrapped", + "unwritten", + "unzip", + "upbeat", + "upchuck", + "upcoming", + "upcountry", + "update", + "upfront", + "upgrade", + "upheaval", + "upheld", + "uphill", + "uphold", + "uplifted", + "uplifting", + "upload", + "upon", + "upper", + "upright", + "uprising", + "upriver", + "uproar", + "uproot", + "upscale", + "upside", + "upstage", + "upstairs", + "upstart", + "upstate", + "upstream", + "upstroke", + "upswing", + "uptake", + "uptight", + "uptown", + "upturned", + "upward", + "upwind", + "uranium", + "urban", + "urchin", + "urethane", + "urgency", + "urgent", + "urging", + "urologist", + "urology", + "usable", + "usage", + "useable", + "used", + "uselessly", + "user", + "usher", + "usual", + "utensil", + "utility", + "utilize", + "utmost", + "utopia", + "utter", + "vacancy", + "vacant", + "vacate", + "vacation", + "vagabond", + "vagrancy", + "vagrantly", + "vaguely", + "vagueness", + "valiant", + "valid", + "valium", + "valley", + "valuables", + "value", + "vanilla", + "vanish", + "vanity", + "vanquish", + "vantage", + "vaporizer", + "variable", + "variably", + "varied", + "variety", + "various", + "varmint", + "varnish", + "varsity", + "varying", + "vascular", + "vaseline", + "vastly", + "vastness", + "veal", + "vegan", + "veggie", + "vehicular", + "velcro", + "velocity", + "velvet", + "vendetta", + "vending", + "vendor", + "veneering", + "vengeful", + "venomous", + "ventricle", + "venture", + "venue", + "venus", + "verbalize", + "verbally", + "verbose", + "verdict", + "verify", + "verse", + "version", + "versus", + "vertebrae", + "vertical", + "vertigo", + "very", + "vessel", + "vest", + "veteran", + "veto", + "vexingly", + "viability", + "viable", + "vibes", + "vice", + "vicinity", + "victory", + "video", + "viewable", + "viewer", + "viewing", + "viewless", + "viewpoint", + "vigorous", + "village", + "villain", + "vindicate", + "vineyard", + "vintage", + "violate", + "violation", + "violator", + "violet", + "violin", + "viper", + "viral", + "virtual", + "virtuous", + "virus", + "visa", + "viscosity", + "viscous", + "viselike", + "visible", + "visibly", + "vision", + "visiting", + "visitor", + "visor", + "vista", + "vitality", + "vitalize", + "vitally", + "vitamins", + "vivacious", + "vividly", + "vividness", + "vixen", + "vocalist", + "vocalize", + "vocally", + "vocation", + "voice", + "voicing", + "void", + "volatile", + "volley", + "voltage", + "volumes", + "voter", + "voting", + "voucher", + "vowed", + "vowel", + "voyage", + "wackiness", + "wad", + "wafer", + "waffle", + "waged", + "wager", + "wages", + "waggle", + "wagon", + "wake", + "waking", + "walk", + "walmart", + "walnut", + "walrus", + "waltz", + "wand", + "wannabe", + "wanted", + "wanting", + "wasabi", + "washable", + "washbasin", + "washboard", + "washbowl", + "washcloth", + "washday", + "washed", + "washer", + "washhouse", + "washing", + "washout", + "washroom", + "washstand", + "washtub", + "wasp", + "wasting", + "watch", + "water", + "waviness", + "waving", + "wavy", + "whacking", + "whacky", + "wham", + "wharf", + "wheat", + "whenever", + "whiff", + "whimsical", + "whinny", + "whiny", + "whisking", + "whoever", + "whole", + "whomever", + "whoopee", + "whooping", + "whoops", + "why", + "wick", + "widely", + "widen", + "widget", + "widow", + "width", + "wieldable", + "wielder", + "wife", + "wifi", + "wikipedia", + "wildcard", + "wildcat", + "wilder", + "wildfire", + "wildfowl", + "wildland", + "wildlife", + "wildly", + "wildness", + "willed", + "willfully", + "willing", + "willow", + "willpower", + "wilt", + "wimp", + "wince", + "wincing", + "wind", + "wing", + "winking", + "winner", + "winnings", + "winter", + "wipe", + "wired", + "wireless", + "wiring", + "wiry", + "wisdom", + "wise", + "wish", + "wisplike", + "wispy", + "wistful", + "wizard", + "wobble", + "wobbling", + "wobbly", + "wok", + "wolf", + "wolverine", + "womanhood", + "womankind", + "womanless", + "womanlike", + "womanly", + "womb", + "woof", + "wooing", + "wool", + "woozy", + "word", + "work", + "worried", + "worrier", + "worrisome", + "worry", + "worsening", + "worshiper", + "worst", + "wound", + "woven", + "wow", + "wrangle", + "wrath", + "wreath", + "wreckage", + "wrecker", + "wrecking", + "wrench", + "wriggle", + "wriggly", + "wrinkle", + "wrinkly", + "wrist", + "writing", + "written", + "wrongdoer", + "wronged", + "wrongful", + "wrongly", + "wrongness", + "wrought", + "xbox", + "xerox", + "yahoo", + "yam", + "yanking", + "yapping", + "yard", + "yarn", + "yeah", + "yearbook", + "yearling", + "yearly", + "yearning", + "yeast", + "yelling", + "yelp", + "yen", + "yesterday", + "yiddish", + "yield", + "yin", + "yippee", + "yo-yo", + "yodel", + "yoga", + "yogurt", + "yonder", + "yoyo", + "yummy", + "zap", + "zealous", + "zebra", + "zen", + "zeppelin", + "zero", + "zestfully", + "zesty", + "zigzagged", + "zipfile", + "zipping", + "zippy", + "zips", + "zit", + "zodiac", + "zombie", + "zone", + "zoning", + "zookeeper", + "zoologist", + "zoology", + "zoom" + }; + } +} diff --git a/src/UWP.Images/UWP.Images.projitems b/src/UWP.Images/UWP.Images.projitems deleted file mode 100644 index b62873882..000000000 --- a/src/UWP.Images/UWP.Images.projitems +++ /dev/null @@ -1,62 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 0be54bbb-7772-4289-bd51-1fdbb0cc2446 - - - UWP.Images - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UWP.Images/UWP.Images.shproj b/src/UWP.Images/UWP.Images.shproj deleted file mode 100644 index ec88a8939..000000000 --- a/src/UWP.Images/UWP.Images.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 0be54bbb-7772-4289-bd51-1fdbb0cc2446 - 14.0 - - - - - - - - diff --git a/src/UWP.Images/android.png b/src/UWP.Images/android.png deleted file mode 100644 index a50f283bd..000000000 Binary files a/src/UWP.Images/android.png and /dev/null differ diff --git a/src/UWP.Images/apple.png b/src/UWP.Images/apple.png deleted file mode 100644 index 8bb1fa0ca..000000000 Binary files a/src/UWP.Images/apple.png and /dev/null differ diff --git a/src/UWP.Images/camera.png b/src/UWP.Images/camera.png deleted file mode 100644 index 23b5d3dc5..000000000 Binary files a/src/UWP.Images/camera.png and /dev/null differ diff --git a/src/UWP.Images/card.png b/src/UWP.Images/card.png deleted file mode 100644 index 8cfa031f6..000000000 Binary files a/src/UWP.Images/card.png and /dev/null differ diff --git a/src/UWP.Images/clipboard.png b/src/UWP.Images/clipboard.png deleted file mode 100644 index bc3b024c8..000000000 Binary files a/src/UWP.Images/clipboard.png and /dev/null differ diff --git a/src/UWP.Images/cloudup.png b/src/UWP.Images/cloudup.png deleted file mode 100644 index f8fd23b8c..000000000 Binary files a/src/UWP.Images/cloudup.png and /dev/null differ diff --git a/src/UWP.Images/cog.png b/src/UWP.Images/cog.png deleted file mode 100644 index c7b86b95e..000000000 Binary files a/src/UWP.Images/cog.png and /dev/null differ diff --git a/src/UWP.Images/cog_alt.png b/src/UWP.Images/cog_alt.png deleted file mode 100644 index de8375761..000000000 Binary files a/src/UWP.Images/cog_alt.png and /dev/null differ diff --git a/src/UWP.Images/cogs.png b/src/UWP.Images/cogs.png deleted file mode 100644 index b12b500c0..000000000 Binary files a/src/UWP.Images/cogs.png and /dev/null differ diff --git a/src/UWP.Images/cogs_selected.png b/src/UWP.Images/cogs_selected.png deleted file mode 100644 index bce1b2939..000000000 Binary files a/src/UWP.Images/cogs_selected.png and /dev/null differ diff --git a/src/UWP.Images/download.png b/src/UWP.Images/download.png deleted file mode 100644 index 631ff3cc0..000000000 Binary files a/src/UWP.Images/download.png and /dev/null differ diff --git a/src/UWP.Images/envelope.png b/src/UWP.Images/envelope.png deleted file mode 100644 index a026d7e76..000000000 Binary files a/src/UWP.Images/envelope.png and /dev/null differ diff --git a/src/UWP.Images/eye.png b/src/UWP.Images/eye.png deleted file mode 100644 index 806d15c9a..000000000 Binary files a/src/UWP.Images/eye.png and /dev/null differ diff --git a/src/UWP.Images/eye_slash.png b/src/UWP.Images/eye_slash.png deleted file mode 100644 index 8c9aa1a94..000000000 Binary files a/src/UWP.Images/eye_slash.png and /dev/null differ diff --git a/src/UWP.Images/fa_lock.png b/src/UWP.Images/fa_lock.png deleted file mode 100644 index c7cc70ffb..000000000 Binary files a/src/UWP.Images/fa_lock.png and /dev/null differ diff --git a/src/UWP.Images/fa_lock_selected.png b/src/UWP.Images/fa_lock_selected.png deleted file mode 100644 index 0b550f6e6..000000000 Binary files a/src/UWP.Images/fa_lock_selected.png and /dev/null differ diff --git a/src/UWP.Images/folder.png b/src/UWP.Images/folder.png deleted file mode 100644 index ae8b657a8..000000000 Binary files a/src/UWP.Images/folder.png and /dev/null differ diff --git a/src/UWP.Images/globe.png b/src/UWP.Images/globe.png deleted file mode 100644 index 56b1670ae..000000000 Binary files a/src/UWP.Images/globe.png and /dev/null differ diff --git a/src/UWP.Images/icon.png b/src/UWP.Images/icon.png deleted file mode 100644 index 8c066db92..000000000 Binary files a/src/UWP.Images/icon.png and /dev/null differ diff --git a/src/UWP.Images/id.png b/src/UWP.Images/id.png deleted file mode 100644 index d05a33229..000000000 Binary files a/src/UWP.Images/id.png and /dev/null differ diff --git a/src/UWP.Images/ion_chevron_left.png b/src/UWP.Images/ion_chevron_left.png deleted file mode 100644 index ec1c903a0..000000000 Binary files a/src/UWP.Images/ion_chevron_left.png and /dev/null differ diff --git a/src/UWP.Images/ion_chevron_right.png b/src/UWP.Images/ion_chevron_right.png deleted file mode 100644 index f9cde2599..000000000 Binary files a/src/UWP.Images/ion_chevron_right.png and /dev/null differ diff --git a/src/UWP.Images/launch.png b/src/UWP.Images/launch.png deleted file mode 100644 index 44131583b..000000000 Binary files a/src/UWP.Images/launch.png and /dev/null differ diff --git a/src/UWP.Images/lightbulb.png b/src/UWP.Images/lightbulb.png deleted file mode 100644 index 8a70cdde4..000000000 Binary files a/src/UWP.Images/lightbulb.png and /dev/null differ diff --git a/src/UWP.Images/lock.png b/src/UWP.Images/lock.png deleted file mode 100644 index fd1bbb793..000000000 Binary files a/src/UWP.Images/lock.png and /dev/null differ diff --git a/src/UWP.Images/login.png b/src/UWP.Images/login.png deleted file mode 100644 index d2ed61aa8..000000000 Binary files a/src/UWP.Images/login.png and /dev/null differ diff --git a/src/UWP.Images/logo.png b/src/UWP.Images/logo.png deleted file mode 100644 index 904731676..000000000 Binary files a/src/UWP.Images/logo.png and /dev/null differ diff --git a/src/UWP.Images/more.png b/src/UWP.Images/more.png deleted file mode 100644 index af9dcd006..000000000 Binary files a/src/UWP.Images/more.png and /dev/null differ diff --git a/src/UWP.Images/more_selected.png b/src/UWP.Images/more_selected.png deleted file mode 100644 index 414ef723f..000000000 Binary files a/src/UWP.Images/more_selected.png and /dev/null differ diff --git a/src/UWP.Images/note.png b/src/UWP.Images/note.png deleted file mode 100644 index 7cb8b4b92..000000000 Binary files a/src/UWP.Images/note.png and /dev/null differ diff --git a/src/UWP.Images/notification_sm.png b/src/UWP.Images/notification_sm.png deleted file mode 100644 index 760eff254..000000000 Binary files a/src/UWP.Images/notification_sm.png and /dev/null differ diff --git a/src/UWP.Images/paperclip.png b/src/UWP.Images/paperclip.png deleted file mode 100644 index 5fcf999ae..000000000 Binary files a/src/UWP.Images/paperclip.png and /dev/null differ diff --git a/src/UWP.Images/photo.png b/src/UWP.Images/photo.png deleted file mode 100644 index 484475a34..000000000 Binary files a/src/UWP.Images/photo.png and /dev/null differ diff --git a/src/UWP.Images/plus.png b/src/UWP.Images/plus.png deleted file mode 100644 index d081b363b..000000000 Binary files a/src/UWP.Images/plus.png and /dev/null differ diff --git a/src/UWP.Images/refresh.png b/src/UWP.Images/refresh.png deleted file mode 100644 index 578f36c07..000000000 Binary files a/src/UWP.Images/refresh.png and /dev/null differ diff --git a/src/UWP.Images/refresh_alt.png b/src/UWP.Images/refresh_alt.png deleted file mode 100644 index decdd33f9..000000000 Binary files a/src/UWP.Images/refresh_alt.png and /dev/null differ diff --git a/src/UWP.Images/refresh_selected.png b/src/UWP.Images/refresh_selected.png deleted file mode 100644 index 47faceff2..000000000 Binary files a/src/UWP.Images/refresh_selected.png and /dev/null differ diff --git a/src/UWP.Images/search.png b/src/UWP.Images/search.png deleted file mode 100644 index c826d6cc2..000000000 Binary files a/src/UWP.Images/search.png and /dev/null differ diff --git a/src/UWP.Images/share.png b/src/UWP.Images/share.png deleted file mode 100644 index f6ea05419..000000000 Binary files a/src/UWP.Images/share.png and /dev/null differ diff --git a/src/UWP.Images/share_tools.png b/src/UWP.Images/share_tools.png deleted file mode 100644 index d034c82e2..000000000 Binary files a/src/UWP.Images/share_tools.png and /dev/null differ diff --git a/src/UWP.Images/smile.png b/src/UWP.Images/smile.png deleted file mode 100644 index 6e7189e69..000000000 Binary files a/src/UWP.Images/smile.png and /dev/null differ diff --git a/src/UWP.Images/tools.png b/src/UWP.Images/tools.png deleted file mode 100644 index 4ae8bbe9e..000000000 Binary files a/src/UWP.Images/tools.png and /dev/null differ diff --git a/src/UWP.Images/tools_selected.png b/src/UWP.Images/tools_selected.png deleted file mode 100644 index 44a5d254e..000000000 Binary files a/src/UWP.Images/tools_selected.png and /dev/null differ diff --git a/src/UWP.Images/trash.png b/src/UWP.Images/trash.png deleted file mode 100644 index 97491ec48..000000000 Binary files a/src/UWP.Images/trash.png and /dev/null differ diff --git a/src/UWP.Images/upload.png b/src/UWP.Images/upload.png deleted file mode 100644 index d192a7ad8..000000000 Binary files a/src/UWP.Images/upload.png and /dev/null differ diff --git a/src/UWP.Images/user.png b/src/UWP.Images/user.png deleted file mode 100644 index 17dfa03fc..000000000 Binary files a/src/UWP.Images/user.png and /dev/null differ diff --git a/src/UWP.Images/yubikey.png b/src/UWP.Images/yubikey.png deleted file mode 100644 index 364c2d06e..000000000 Binary files a/src/UWP.Images/yubikey.png and /dev/null differ diff --git a/src/UWP/App.xaml b/src/UWP/App.xaml deleted file mode 100644 index 01dceea30..000000000 --- a/src/UWP/App.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/src/UWP/App.xaml.cs b/src/UWP/App.xaml.cs deleted file mode 100644 index 4f302cab7..000000000 --- a/src/UWP/App.xaml.cs +++ /dev/null @@ -1,150 +0,0 @@ -using Acr.UserDialogs; -using Bit.App.Abstractions; -using Bit.App.Repositories; -using Bit.App.Services; -using Bit.UWP.Services; -using FFImageLoading.Forms.WinUWP; -using Plugin.Connectivity; -using Plugin.Fingerprint; -using Plugin.Settings.Abstractions; -using SimpleInjector; -using System; -using System.Collections.Generic; -using System.Reflection; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Activation; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Navigation; -using XLabs.Ioc; -using XLabs.Ioc.SimpleInjectorContainer; -using FFImageLoading.Forms; - -namespace Bit.UWP -{ - sealed partial class App : Application - { - public App() - { - InitializeComponent(); - Suspending += OnSuspending; - if(!Resolver.IsSet) - { - SetIoc(); - } - } - - public ISettings Settings { get; set; } - - protected override void OnLaunched(LaunchActivatedEventArgs e) - { - ZXing.Net.Mobile.Forms.WindowsUniversal.ZXingScannerViewRenderer.Init(); - - var rootFrame = Window.Current.Content as Frame; - if(rootFrame == null) - { - rootFrame = new Frame(); - rootFrame.NavigationFailed += OnNavigationFailed; - - var assembliesToInclude = new List() - { - typeof(CachedImage).GetTypeInfo().Assembly, - typeof(CachedImageRenderer).GetTypeInfo().Assembly, - typeof(Controls.ExtendedTableViewRenderer).GetTypeInfo().Assembly - }; - Xamarin.Forms.Forms.Init(e, assembliesToInclude); - - ((Style)Resources["TabbedPageStyle"]).Setters[0] = ((Style)Resources["TabbedPageStyle2"]).Setters[0]; - - if(e.PreviousExecutionState == ApplicationExecutionState.Terminated) - { - //TODO: Load state from previously suspended application - } - - Window.Current.Content = rootFrame; - } - - if(e.PrelaunchActivated == false) - { - if(rootFrame.Content == null) - { - rootFrame.Navigate(typeof(MainPage), e.Arguments); - } - - Window.Current.Activate(); - } - } - - void OnNavigationFailed(object sender, NavigationFailedEventArgs e) - { - throw new Exception("Failed to load Page " + e.SourcePageType.FullName); - } - - private void OnSuspending(object sender, SuspendingEventArgs e) - { - var deferral = e.SuspendingOperation.GetDeferral(); - //TODO: Save application state and stop any background activity - deferral.Complete(); - } - - private void SetIoc() - { - var container = new Container(); - - // Services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Repositories - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Other - container.RegisterSingleton(CrossConnectivity.Current); - container.RegisterSingleton(UserDialogs.Instance); - container.RegisterSingleton(CrossFingerprint.Current); - - container.RegisterSingleton(Plugin.Settings.CrossSettings.Current); - - // Push - container.RegisterSingleton(); - container.RegisterSingleton(); - - CachedImageRenderer.Init(); - Resolver.SetResolver(new SimpleInjectorResolver(container)); - } - } -} diff --git a/src/UWP/Assets/BadgeLogo.scale-100.png b/src/UWP/Assets/BadgeLogo.scale-100.png deleted file mode 100644 index 9f028b2bb..000000000 Binary files a/src/UWP/Assets/BadgeLogo.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/BadgeLogo.scale-125.png b/src/UWP/Assets/BadgeLogo.scale-125.png deleted file mode 100644 index 71cd77b2b..000000000 Binary files a/src/UWP/Assets/BadgeLogo.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/BadgeLogo.scale-150.png b/src/UWP/Assets/BadgeLogo.scale-150.png deleted file mode 100644 index 8c895a28c..000000000 Binary files a/src/UWP/Assets/BadgeLogo.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/BadgeLogo.scale-200.png b/src/UWP/Assets/BadgeLogo.scale-200.png deleted file mode 100644 index bbb8f64ca..000000000 Binary files a/src/UWP/Assets/BadgeLogo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/BadgeLogo.scale-400.png b/src/UWP/Assets/BadgeLogo.scale-400.png deleted file mode 100644 index fcccc372a..000000000 Binary files a/src/UWP/Assets/BadgeLogo.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/LargeTile.scale-100.png b/src/UWP/Assets/LargeTile.scale-100.png deleted file mode 100644 index ba92d5f66..000000000 Binary files a/src/UWP/Assets/LargeTile.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/LargeTile.scale-125.png b/src/UWP/Assets/LargeTile.scale-125.png deleted file mode 100644 index 638b3d7d9..000000000 Binary files a/src/UWP/Assets/LargeTile.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/LargeTile.scale-150.png b/src/UWP/Assets/LargeTile.scale-150.png deleted file mode 100644 index 2883250e0..000000000 Binary files a/src/UWP/Assets/LargeTile.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/LargeTile.scale-200.png b/src/UWP/Assets/LargeTile.scale-200.png deleted file mode 100644 index b12d65920..000000000 Binary files a/src/UWP/Assets/LargeTile.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/LargeTile.scale-400.png b/src/UWP/Assets/LargeTile.scale-400.png deleted file mode 100644 index 01604baf2..000000000 Binary files a/src/UWP/Assets/LargeTile.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/LockScreenLogo.scale-200.png b/src/UWP/Assets/LockScreenLogo.scale-200.png deleted file mode 100644 index 9c4340090..000000000 Binary files a/src/UWP/Assets/LockScreenLogo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/SmallTile.scale-100.png b/src/UWP/Assets/SmallTile.scale-100.png deleted file mode 100644 index 56c7bd413..000000000 Binary files a/src/UWP/Assets/SmallTile.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/SmallTile.scale-125.png b/src/UWP/Assets/SmallTile.scale-125.png deleted file mode 100644 index ae0cdfcf5..000000000 Binary files a/src/UWP/Assets/SmallTile.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/SmallTile.scale-150.png b/src/UWP/Assets/SmallTile.scale-150.png deleted file mode 100644 index 0f3709657..000000000 Binary files a/src/UWP/Assets/SmallTile.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/SmallTile.scale-200.png b/src/UWP/Assets/SmallTile.scale-200.png deleted file mode 100644 index 928ccc5dc..000000000 Binary files a/src/UWP/Assets/SmallTile.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/SmallTile.scale-400.png b/src/UWP/Assets/SmallTile.scale-400.png deleted file mode 100644 index 78bd32f18..000000000 Binary files a/src/UWP/Assets/SmallTile.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/SplashScreen.scale-100.png b/src/UWP/Assets/SplashScreen.scale-100.png deleted file mode 100644 index aa3e8e693..000000000 Binary files a/src/UWP/Assets/SplashScreen.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/SplashScreen.scale-125.png b/src/UWP/Assets/SplashScreen.scale-125.png deleted file mode 100644 index 5db71549e..000000000 Binary files a/src/UWP/Assets/SplashScreen.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/SplashScreen.scale-150.png b/src/UWP/Assets/SplashScreen.scale-150.png deleted file mode 100644 index 3958e0078..000000000 Binary files a/src/UWP/Assets/SplashScreen.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/SplashScreen.scale-200.png b/src/UWP/Assets/SplashScreen.scale-200.png deleted file mode 100644 index ca98370f3..000000000 Binary files a/src/UWP/Assets/SplashScreen.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/SplashScreen.scale-400.png b/src/UWP/Assets/SplashScreen.scale-400.png deleted file mode 100644 index f3bf7bd5f..000000000 Binary files a/src/UWP/Assets/SplashScreen.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/Square150x150Logo.scale-100.png b/src/UWP/Assets/Square150x150Logo.scale-100.png deleted file mode 100644 index a71a22df4..000000000 Binary files a/src/UWP/Assets/Square150x150Logo.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/Square150x150Logo.scale-125.png b/src/UWP/Assets/Square150x150Logo.scale-125.png deleted file mode 100644 index 83e911f90..000000000 Binary files a/src/UWP/Assets/Square150x150Logo.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/Square150x150Logo.scale-150.png b/src/UWP/Assets/Square150x150Logo.scale-150.png deleted file mode 100644 index 85dec1191..000000000 Binary files a/src/UWP/Assets/Square150x150Logo.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/Square150x150Logo.scale-200.png b/src/UWP/Assets/Square150x150Logo.scale-200.png deleted file mode 100644 index 888c8b73a..000000000 Binary files a/src/UWP/Assets/Square150x150Logo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/Square150x150Logo.scale-400.png b/src/UWP/Assets/Square150x150Logo.scale-400.png deleted file mode 100644 index dca7b9661..000000000 Binary files a/src/UWP/Assets/Square150x150Logo.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png deleted file mode 100644 index 0bab0fc00..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png deleted file mode 100644 index 4e15775a6..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-32.png deleted file mode 100644 index 0aa710a53..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-32.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png deleted file mode 100644 index e9a62804c..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.scale-100.png b/src/UWP/Assets/Square44x44Logo.scale-100.png deleted file mode 100644 index e584e3b04..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.scale-125.png b/src/UWP/Assets/Square44x44Logo.scale-125.png deleted file mode 100644 index 5fbadb9b6..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.scale-150.png b/src/UWP/Assets/Square44x44Logo.scale-150.png deleted file mode 100644 index a5b117212..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.scale-200.png b/src/UWP/Assets/Square44x44Logo.scale-200.png deleted file mode 100644 index b2465c78f..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.scale-400.png b/src/UWP/Assets/Square44x44Logo.scale-400.png deleted file mode 100644 index 6c6971928..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-16.png b/src/UWP/Assets/Square44x44Logo.targetsize-16.png deleted file mode 100644 index b8921b615..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-16.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-24.png b/src/UWP/Assets/Square44x44Logo.targetsize-24.png deleted file mode 100644 index b07c219ec..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-24.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png deleted file mode 100644 index dbf55071a..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-256.png b/src/UWP/Assets/Square44x44Logo.targetsize-256.png deleted file mode 100644 index 1b7270c5e..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-256.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-32.png b/src/UWP/Assets/Square44x44Logo.targetsize-32.png deleted file mode 100644 index 5ffe87fb3..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-32.png and /dev/null differ diff --git a/src/UWP/Assets/Square44x44Logo.targetsize-48.png b/src/UWP/Assets/Square44x44Logo.targetsize-48.png deleted file mode 100644 index 344df913f..000000000 Binary files a/src/UWP/Assets/Square44x44Logo.targetsize-48.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.backup.png b/src/UWP/Assets/StoreLogo.backup.png deleted file mode 100644 index 375b5cbdd..000000000 Binary files a/src/UWP/Assets/StoreLogo.backup.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.scale-100.png b/src/UWP/Assets/StoreLogo.scale-100.png deleted file mode 100644 index 3fa97c03f..000000000 Binary files a/src/UWP/Assets/StoreLogo.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.scale-125.png b/src/UWP/Assets/StoreLogo.scale-125.png deleted file mode 100644 index 54cf78cc2..000000000 Binary files a/src/UWP/Assets/StoreLogo.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.scale-150.png b/src/UWP/Assets/StoreLogo.scale-150.png deleted file mode 100644 index 6c445cbbf..000000000 Binary files a/src/UWP/Assets/StoreLogo.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.scale-200.png b/src/UWP/Assets/StoreLogo.scale-200.png deleted file mode 100644 index b03617149..000000000 Binary files a/src/UWP/Assets/StoreLogo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/StoreLogo.scale-400.png b/src/UWP/Assets/StoreLogo.scale-400.png deleted file mode 100644 index afcca1ea0..000000000 Binary files a/src/UWP/Assets/StoreLogo.scale-400.png and /dev/null differ diff --git a/src/UWP/Assets/Wide310x150Logo.scale-100.png b/src/UWP/Assets/Wide310x150Logo.scale-100.png deleted file mode 100644 index 311dd0588..000000000 Binary files a/src/UWP/Assets/Wide310x150Logo.scale-100.png and /dev/null differ diff --git a/src/UWP/Assets/Wide310x150Logo.scale-125.png b/src/UWP/Assets/Wide310x150Logo.scale-125.png deleted file mode 100644 index b208a57db..000000000 Binary files a/src/UWP/Assets/Wide310x150Logo.scale-125.png and /dev/null differ diff --git a/src/UWP/Assets/Wide310x150Logo.scale-150.png b/src/UWP/Assets/Wide310x150Logo.scale-150.png deleted file mode 100644 index 915d6be76..000000000 Binary files a/src/UWP/Assets/Wide310x150Logo.scale-150.png and /dev/null differ diff --git a/src/UWP/Assets/Wide310x150Logo.scale-200.png b/src/UWP/Assets/Wide310x150Logo.scale-200.png deleted file mode 100644 index aa3e8e693..000000000 Binary files a/src/UWP/Assets/Wide310x150Logo.scale-200.png and /dev/null differ diff --git a/src/UWP/Assets/Wide310x150Logo.scale-400.png b/src/UWP/Assets/Wide310x150Logo.scale-400.png deleted file mode 100644 index ca98370f3..000000000 Binary files a/src/UWP/Assets/Wide310x150Logo.scale-400.png and /dev/null differ diff --git a/src/UWP/Controls/ExtendedTableViewRenderer.cs b/src/UWP/Controls/ExtendedTableViewRenderer.cs deleted file mode 100644 index faf33080a..000000000 --- a/src/UWP/Controls/ExtendedTableViewRenderer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.ComponentModel; -using Bit.App.Controls; -using Bit.UWP.Controls; -using Xamarin.Forms; -using Xamarin.Forms.Platform.UWP; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; - - -[assembly: ExportRenderer(typeof(ExtendedTableView), typeof(ExtendedTableViewRenderer))] -namespace Bit.UWP.Controls -{ - public class ExtendedTableViewRenderer : TableViewRenderer - { - public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) - { - var baseSize = new Size(Control.Width, Control.Height); - - return new SizeRequest(new Size(baseSize.Width, baseSize.Height)); - } - - protected override void OnElementChanged(ElementChangedEventArgs e) - { - base.OnElementChanged(e); - } - } -} diff --git a/src/UWP/IconConverter.cs b/src/UWP/IconConverter.cs deleted file mode 100644 index d6ffc4d21..000000000 --- a/src/UWP/IconConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Windows.UI.Xaml.Data; - -namespace Bit.UWP -{ - public class IconConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, string language) - { - if(value != null && value is Xamarin.Forms.FileImageSource) - { - return ((Xamarin.Forms.FileImageSource)value).File; - } - - return null; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/UWP/MainPage.xaml b/src/UWP/MainPage.xaml deleted file mode 100644 index 2af72d753..000000000 --- a/src/UWP/MainPage.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/src/UWP/MainPage.xaml.cs b/src/UWP/MainPage.xaml.cs deleted file mode 100644 index ac310994f..000000000 --- a/src/UWP/MainPage.xaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Acr.UserDialogs; -using Bit.App.Abstractions; -using Plugin.Connectivity.Abstractions; -using Plugin.Settings.Abstractions; -using Xamarin.Forms.Platform.UWP; -using XLabs.Ioc; - -namespace Bit.UWP -{ - public sealed partial class MainPage : WindowsPage - { - public MainPage() - { - InitializeComponent(); - LoadApplication(new Bit.App.App( - null, - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve())); - } - } -} diff --git a/src/UWP/Package.appxmanifest b/src/UWP/Package.appxmanifest deleted file mode 100644 index 6a2906435..000000000 --- a/src/UWP/Package.appxmanifest +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Bitwarden - 8bit Solutions LLC - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UWP/Properties/AssemblyInfo.cs b/src/UWP/Properties/AssemblyInfo.cs deleted file mode 100644 index 6a814fbd3..000000000 --- a/src/UWP/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("UWP")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("UWP")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: ComVisible(false)] \ No newline at end of file diff --git a/src/UWP/Properties/Default.rd.xml b/src/UWP/Properties/Default.rd.xml deleted file mode 100644 index af00722cd..000000000 --- a/src/UWP/Properties/Default.rd.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UWP/Services/AppInfoService.cs b/src/UWP/Services/AppInfoService.cs deleted file mode 100644 index ab39f0c7e..000000000 --- a/src/UWP/Services/AppInfoService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Bit.App.Abstractions; -using Windows.ApplicationModel; - -namespace Bit.UWP.Services -{ - public class AppInfoService : IAppInfoService - { - public string Build => Package.Current.Id.Version.Build.ToString(); - - public string Version - { - get - { - var version = Package.Current.Id.Version; - return $"{version.Major}.{version.Minor}.{version.Build}"; - } - } - - public bool AutofillAccessibilityServiceEnabled => false; - public bool AutofillServiceEnabled => false; - } -} diff --git a/src/UWP/Services/DeviceActionService.cs b/src/UWP/Services/DeviceActionService.cs deleted file mode 100644 index 3fdb70374..000000000 --- a/src/UWP/Services/DeviceActionService.cs +++ /dev/null @@ -1,203 +0,0 @@ -using Acr.UserDialogs; -using Bit.App.Abstractions; -using Bit.App.Models.Page; -using Bit.App.Resources; -using Coding4Fun.Toolkit.Controls; -using System; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using System.Threading.Tasks; -using Windows.ApplicationModel.Core; -using Windows.ApplicationModel.DataTransfer; -using Windows.Storage; -using Windows.System; -using Windows.UI; -using Windows.UI.Core; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Media; - -namespace Bit.UWP.Services -{ - public class DeviceActionService : IDeviceActionService - { - private readonly IUserDialogs _userDialogs; - - public DeviceActionService(IUserDialogs userDialogs) - { - _userDialogs = userDialogs; - } - - public bool CanOpenFile(string fileName) => true; - - public void ClearCache() - { - Task.Run(async () => - { - foreach(var item in await ApplicationData.Current.LocalCacheFolder.GetItemsAsync()) - { - await item.DeleteAsync(); - } - }).Wait(); - } - - public void CopyToClipboard(string text) - { - var dataPackage = new DataPackage - { - RequestedOperation = DataPackageOperation.Copy - }; - dataPackage.SetText(text); - Clipboard.SetContent(dataPackage); - } - - public bool OpenFile(byte[] fileData, string id, string fileName) - { - try - { - //the method is synchronous in the interface, so the async method are run synchronously here - var storageFolder = ApplicationData.Current.LocalCacheFolder; - var file = storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().Result; - FileIO.WriteBytesAsync(file, fileData).AsTask().Wait(); - Launcher.LaunchFileAsync(file, new LauncherOptions { DisplayApplicationPicker = true }).AsTask().Wait(); - - return true; - } - catch - { - return false; - } - } - - public Task SelectFileAsync() - { - var picker = new Windows.Storage.Pickers.FileOpenPicker - { - ViewMode = Windows.Storage.Pickers.PickerViewMode.Thumbnail, - SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary, - FileTypeFilter = { "*" } - }; - - return CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => - { - var file = await picker.PickSingleFileAsync(); - if(file != null) - { - await SelectFileResult(file); - } - }).AsTask(); - } - - private async Task SelectFileResult(StorageFile file) - { - var buffer = await FileIO.ReadBufferAsync(file); - Xamarin.Forms.MessagingCenter.Send(Xamarin.Forms.Application.Current, "SelectFileResult", - new Tuple(buffer.ToArray(), file.Name)); - } - - public void Autofill(VaultListPageModel.Cipher cipher) - { - throw new NotImplementedException(); - } - - public void CloseAutofill() - { - throw new NotImplementedException(); - } - - public void Background() - { - // do nothing - } - - public void RateApp() - { - // do nothing - } - - public void DismissKeyboard() - { - // do nothing - } - - public void LaunchApp(string appName) - { - // do nothing - } - - public void OpenAccessibilitySettings() - { - throw new NotImplementedException(); - } - - public void OpenAutofillSettings() - { - throw new NotImplementedException(); - } - - public void Toast(string text, bool longDuration = false) - { - new ToastPrompt - { - Message = text, - TextWrapping = TextWrapping.Wrap, - MillisecondsUntilHidden = Convert.ToInt32(longDuration ? 5 : 2) * 1000, - Background = new SolidColorBrush(Color.FromArgb(240, 210, 214, 222)), - Foreground = new SolidColorBrush(Color.FromArgb(240, 60, 141, 188)), - Margin = new Thickness(0, 0, 0, 100), - HorizontalContentAlignment = HorizontalAlignment.Center, - VerticalContentAlignment = VerticalAlignment.Center, - Stretch = Stretch.Uniform, - IsHitTestVisible = false - }.Show(); - } - - public Task ShowLoadingAsync(string text) - { - _userDialogs.ShowLoading(text, MaskType.Black); - return Task.FromResult(0); - } - - public Task HideLoadingAsync() - { - _userDialogs.HideLoading(); - return Task.FromResult(0); - } - - public Task LaunchAppAsync(string appName, Xamarin.Forms.Page page) - { - throw new NotImplementedException(); - } - - public async Task DisplayPromptAync(string title = null, string description = null, string text = null) - { - var result = await _userDialogs.PromptAsync(new PromptConfig - { - Title = title, - InputType = InputType.Default, - OkText = AppResources.Ok, - CancelText = AppResources.Cancel, - Message = description, - Text = text - }); - - return result.Ok ? result.Value ?? string.Empty : null; - } - - public async Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) - { - if(!string.IsNullOrWhiteSpace(message)) - { - if(string.IsNullOrWhiteSpace(title)) - { - title = message; - } - else - { - title = $"{title}: {message}"; - } - } - - return await _userDialogs.ActionSheetAsync(title, cancel, null, null, buttons.ToArray()); - } - } -} diff --git a/src/UWP/Services/DeviceInfoService.cs b/src/UWP/Services/DeviceInfoService.cs deleted file mode 100644 index 8a7f34918..000000000 --- a/src/UWP/Services/DeviceInfoService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Bit.App.Abstractions; -using Microsoft.Toolkit.Uwp.Helpers; -using System; -using System.Linq; -using System.Threading.Tasks; -using Windows.Graphics.Display; -using Windows.Devices.SmartCards; -using Windows.Devices.Enumeration; - -namespace Bit.UWP.Services -{ - public class DeviceInfoService : IDeviceInfoService - { - private const string SmartCardEmulatorType = "Windows.Devices.SmartCards.SmartCardEmulator"; - - public string Type => Xamarin.Forms.Device.UWP; - public string Model => SystemInformation.DeviceModel; - public int Version => SystemInformation.OperatingSystemVersion.Build; - public float Scale => (float)DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel; - - public bool NfcEnabled - { - get - { - if(!Windows.Foundation.Metadata.ApiInformation.IsTypePresent(SmartCardEmulatorType)) - { - return false; - } - - return Task.Run(async () => await SmartCardEmulator.GetDefaultAsync()).Result != null; - } - } - - public bool HasCamera - { - get - { - var cameraList = Task.Run(async () => - await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture)).Result; - return cameraList?.Any() ?? false; - } - } - - public bool AutofillServiceSupported => false; - public bool HasFaceIdSupport => false; - } -} diff --git a/src/UWP/Services/GoogleAnalyticsService.cs b/src/UWP/Services/GoogleAnalyticsService.cs deleted file mode 100644 index 550e5f729..000000000 --- a/src/UWP/Services/GoogleAnalyticsService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bit.App.Abstractions; -using System; - -namespace Bit.UWP.Services -{ - public class GoogleAnalyticsService : IGoogleAnalyticsService - { - public void Dispatch(Action completionHandler = null) - { - } - - public void SetAppOptOut(bool optOut) - { - } - - public void TrackAppEvent(string eventName, string label = null) - { - } - - public void TrackEvent(string category, string eventName, string label = null) - { - } - - public void TrackException(string message, bool fatal) - { - } - - public void TrackExtensionEvent(string eventName, string label = null) - { - } - - public void TrackPage(string pageName) - { - } - } -} diff --git a/src/UWP/Services/HttpService.cs b/src/UWP/Services/HttpService.cs deleted file mode 100644 index 9f4613123..000000000 --- a/src/UWP/Services/HttpService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.App.Abstractions; -using Bit.App; - -namespace Bit.UWP.Services -{ - public class HttpService : IHttpService - { - public ApiHttpClient ApiClient => new ApiHttpClient(); - public IdentityHttpClient IdentityClient => new IdentityHttpClient(); - } -} diff --git a/src/UWP/Services/KeyDerivationService.cs b/src/UWP/Services/KeyDerivationService.cs deleted file mode 100644 index 49b660279..000000000 --- a/src/UWP/Services/KeyDerivationService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Bit.App.Abstractions; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Security.Cryptography.Core; - -namespace Bit.UWP.Services -{ - public class KeyDerivationService : IKeyDerivationService - { - private const int KeyLength = 32; // 32 bytes - - public byte[] DeriveKey(byte[] password, byte[] salt, uint rounds) - { - var buffSalt = salt.AsBuffer(); - var buffPassword = password.AsBuffer(); - var provider = KeyDerivationAlgorithmProvider.OpenAlgorithm(KeyDerivationAlgorithmNames.Pbkdf2Sha256); - var pbkdf2Params = KeyDerivationParameters.BuildForPbkdf2(buffSalt, rounds); - var keyOriginal = provider.CreateKey(buffPassword); - - var keyDerived = CryptographicEngine.DeriveKeyMaterial(keyOriginal, pbkdf2Params, KeyLength); - return keyDerived.ToArray(); - } - } -} diff --git a/src/UWP/Services/LocalizeService.cs b/src/UWP/Services/LocalizeService.cs deleted file mode 100644 index 00d552553..000000000 --- a/src/UWP/Services/LocalizeService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Bit.App.Abstractions; -using System.Globalization; -using Windows.Globalization; - -namespace Bit.UWP.Services -{ - public class LocalizeService : ILocalizeService - { - public CultureInfo GetCurrentCultureInfo() - { - return CultureInfo.CurrentCulture; - } - - public void SetLocale(CultureInfo locale) - { - CultureInfo.CurrentCulture = locale; - CultureInfo.CurrentUICulture = locale; - CultureInfo.DefaultThreadCurrentCulture = locale; - CultureInfo.DefaultThreadCurrentUICulture = locale; - - ApplicationLanguages.PrimaryLanguageOverride = locale.TwoLetterISOLanguageName; - - } - } -} diff --git a/src/UWP/Services/LogService.cs b/src/UWP/Services/LogService.cs deleted file mode 100644 index bdfe90263..000000000 --- a/src/UWP/Services/LogService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Bit.App.Abstractions; -using System.Diagnostics; - -namespace Bit.UWP.Services -{ - public class LogService : ILogService - { - public void WriteLine(string message) => Debug.WriteLine(message); - } -} diff --git a/src/UWP/Services/SecureStorageService.cs b/src/UWP/Services/SecureStorageService.cs deleted file mode 100644 index 2b0871f91..000000000 --- a/src/UWP/Services/SecureStorageService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Bit.App.Abstractions; -using System; -using Windows.Security.Credentials; - -namespace Bit.UWP.Services -{ - public class SecureStorageService : ISecureStorageService - { - private const string ResourceName = "bitwarden"; - private readonly PasswordVault _vault = new PasswordVault(); - - public bool Contains(string key) - { - try - { - return _vault.Retrieve(ResourceName, key) != null; - } - catch - { - return false; - } - } - - public void Delete(string key) - { - var entry = _vault.Retrieve(ResourceName, key); - if(entry != null) - { - _vault.Remove(entry); - } - } - - public byte[] Retrieve(string key) - { - try - { - var entry = _vault.Retrieve(ResourceName, key); - if(entry != null) - { - return Convert.FromBase64String(entry.Password); - } - else - { - return null; - } - } - catch - { - return null; - } - } - - public void Store(string key, byte[] dataBytes) - { - var data = Convert.ToBase64String(dataBytes); - _vault.Add(new PasswordCredential(ResourceName, key, data)); - } - } -} diff --git a/src/UWP/Services/SqlService.cs b/src/UWP/Services/SqlService.cs deleted file mode 100644 index e672379ef..000000000 --- a/src/UWP/Services/SqlService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Bit.App.Abstractions; -using SQLite; -using System.IO; -using Windows.Storage; - -namespace Bit.UWP.Services -{ - public class SqlService : ISqlService - { - public SQLiteConnection GetConnection() - { - var sqliteFilename = "bitwarden.db3"; - var path = Path.Combine(ApplicationData.Current.LocalFolder.Path, sqliteFilename); - var conn = new SQLiteConnection(path, - SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | - SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.SharedCache); - - // Return the database connection - return conn; - } - } -} \ No newline at end of file diff --git a/src/UWP/Services/UwpPushNotificationService.cs b/src/UWP/Services/UwpPushNotificationService.cs deleted file mode 100644 index ad8cbb0de..000000000 --- a/src/UWP/Services/UwpPushNotificationService.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Bit.App.Abstractions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Diagnostics; -using Windows.Networking.PushNotifications; -using Xamarin.Forms; - -namespace Bit.UWP.Services -{ - public class UwpPushNotificationService : IPushNotificationService - { - private PushNotificationChannel _channel; - private JsonSerializer _serializer = new JsonSerializer - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }; - private readonly IPushNotificationListener _pushNotificationListener; - - public UwpPushNotificationService(IPushNotificationListener pushNotificationListener) - { - _pushNotificationListener = pushNotificationListener; - } - - public string Token => _channel?.Uri.ToString(); - - public void Register() - { - Debug.WriteLine("Creating Push Notification Channel For Application"); - var channelTask = PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync().AsTask(); - channelTask.Wait(); - - Debug.WriteLine("Creating Push Notification Channel For Application - Done"); - _channel = channelTask.Result; - - Debug.WriteLine("Registering call back for Push Notification Channel"); - _channel.PushNotificationReceived += Channel_PushNotificationReceived; - - _pushNotificationListener.OnRegistered(Token, Device.UWP); - } - - private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args) - { - Debug.WriteLine("Push Notification Received " + args.NotificationType); - JObject jobject = null; - - switch(args.NotificationType) - { - case PushNotificationType.Badge: - jobject = JObject.FromObject(args.BadgeNotification, _serializer); - break; - case PushNotificationType.Raw: - jobject = JObject.FromObject(args.RawNotification, _serializer); - break; - case PushNotificationType.Tile: - jobject = JObject.FromObject(args.TileNotification, _serializer); - break; - case PushNotificationType.TileFlyout: - jobject = JObject.FromObject(args.TileNotification, _serializer); - break; - case PushNotificationType.Toast: - jobject = JObject.FromObject(args.ToastNotification, _serializer); - break; - } - - Debug.WriteLine("Sending JObject to PushNotificationListener " + args.NotificationType); - _pushNotificationListener.OnMessage(jobject, Device.UWP); - } - - public void Unregister() - { - if(_channel != null) - { - _channel.PushNotificationReceived -= Channel_PushNotificationReceived; - _channel = null; - } - - _pushNotificationListener.OnUnregistered(Device.UWP); - } - } -} diff --git a/src/UWP/Styles.xaml b/src/UWP/Styles.xaml deleted file mode 100644 index 50f72c257..000000000 --- a/src/UWP/Styles.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/UWP/UWP.csproj b/src/UWP/UWP.csproj deleted file mode 100644 index caf6a0efd..000000000 --- a/src/UWP/UWP.csproj +++ /dev/null @@ -1,223 +0,0 @@ - - - - - Debug - x86 - {3A2D5669-ED71-4F2B-BA85-2D36BAA05141} - AppContainerExe - Properties - Bit.UWP - Bit.UWP - en-US - UAP - 10.0.16299.0 - 10.0.16299.0 - 14 - 512 - {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - true - UWP_TemporaryKey.pfx - - - true - bin\x86\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - x86 - false - prompt - true - - - bin\x86\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - x86 - false - prompt - true - true - - - true - bin\ARM\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - ARM - false - prompt - true - - - bin\ARM\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - ARM - false - prompt - true - true - - - true - bin\x64\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - x64 - false - prompt - true - - - bin\x64\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - true - ;2008 - pdbonly - x64 - false - prompt - true - true - - - PackageReference - - - - App.xaml - - - - - MainPage.xaml - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - - - 6.5.1 - - - 6.0.5 - - - 2.1.1 - - - 4.0.12 - - - 2.0.5782 - - - - - Windows Mobile Extensions for the UWP - - - - - {8a279ee4-4537-4656-9c93-44945e594556} - App - - - - - 14.0 - - - \ No newline at end of file diff --git a/src/iOS.Autofill/AppDelegate.cs b/src/iOS.Autofill/AppDelegate.cs deleted file mode 100644 index b5c1b98df..000000000 --- a/src/iOS.Autofill/AppDelegate.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; - -using Foundation; -using UIKit; - -namespace Bit.iOS.Autofill -{ - // The UIApplicationDelegate for the application. This class is responsible for launching the - // User Interface of the application, as well as listening (and optionally responding) to - // application events from iOS. - [Register("AppDelegate")] - public partial class AppDelegate : UIApplicationDelegate - { - // class-level declarations - - public override UIWindow Window - { - get; set; - } - - // This method is invoked when the application is about to move from active to inactive state. - // OpenGL applications should use this method to pause. - public override void OnResignActivation(UIApplication application) - { - } - - // This method should be used to release shared resources and it should store the application state. - // If your application supports background exection this method is called instead of WillTerminate - // when the user quits. - public override void DidEnterBackground(UIApplication application) - { - } - - // This method is called as part of the transiton from background to active state. - public override void WillEnterForeground(UIApplication application) - { - } - - // This method is called when the application is about to terminate. Save data, if needed. - public override void WillTerminate(UIApplication application) - { - } - } -} diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs deleted file mode 100644 index 8fe2bfec5..000000000 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ /dev/null @@ -1,372 +0,0 @@ -using AuthenticationServices; -using Bit.App.Abstractions; -using Bit.App.Repositories; -using Bit.App.Resources; -using Bit.App.Services; -using Bit.App.Utilities; -using Bit.iOS.Autofill.Models; -using Bit.iOS.Core; -using Bit.iOS.Core.Services; -using Bit.iOS.Core.Utilities; -using Foundation; -using Plugin.Connectivity; -using Plugin.Fingerprint; -using Plugin.Settings.Abstractions; -using SimpleInjector; -using System; -using UIKit; -using XLabs.Ioc; -using XLabs.Ioc.SimpleInjectorContainer; - -namespace Bit.iOS.Autofill -{ - public partial class CredentialProviderViewController : ASCredentialProviderViewController - { - private Context _context; - private bool _setupHockeyApp = false; - private IGoogleAnalyticsService _googleAnalyticsService; - private ISettings _settings; - - public CredentialProviderViewController(IntPtr handle) : base(handle) - { } - - public override void ViewDidLoad() - { - SetIoc(); - SetCulture(); - base.ViewDidLoad(); - _context = new Context(); - _context.ExtContext = ExtensionContext; - _googleAnalyticsService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - - if(!_setupHockeyApp) - { - var appIdService = Resolver.Resolve(); - var crashManagerDelegate = new HockeyAppCrashManagerDelegate(appIdService, Resolver.Resolve()); - var manager = HockeyApp.iOS.BITHockeyManager.SharedHockeyManager; - manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); - manager.CrashManager.CrashManagerStatus = HockeyApp.iOS.BITCrashManagerStatus.AutoSend; - manager.UserId = appIdService.AppId; - manager.StartManager(); - manager.Authenticator.AuthenticateInstallation(); - _setupHockeyApp = true; - } - } - - public override void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers) - { - _context.ServiceIdentifiers = serviceIdentifiers; - if(serviceIdentifiers.Length > 0) - { - var uri = serviceIdentifiers[0].Identifier; - if(serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) - { - uri = string.Concat("https://", uri); - } - _context.UrlString = uri; - } - if(!CheckAuthed()) - { - return; - } - - var lockService = Resolver.Resolve(); - var lockType = lockService.GetLockTypeAsync(false).GetAwaiter().GetResult(); - switch(lockType) - { - case App.Enums.LockType.Fingerprint: - PerformSegue("lockFingerprintSegue", this); - break; - case App.Enums.LockType.PIN: - PerformSegue("lockPinSegue", this); - break; - case App.Enums.LockType.Password: - PerformSegue("lockPasswordSegue", this); - break; - default: - if(_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) - { - PerformSegue("loginSearchSegue", this); - } - else - { - PerformSegue("loginListSegue", this); - } - break; - } - } - - public override void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) - { - bool canGetCredentials = false; - var authService = Resolver.Resolve(); - if(authService.IsAuthenticated) - { - var lockService = Resolver.Resolve(); - var lockType = lockService.GetLockTypeAsync(false).GetAwaiter().GetResult(); - canGetCredentials = lockType == App.Enums.LockType.Fingerprint || lockType == App.Enums.LockType.None; - } - - if(!canGetCredentials) - { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); - ExtensionContext.CancelRequest(err); - return; - } - _context.CredentialIdentity = credentialIdentity; - ProvideCredential(); - } - - public override void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) - { - if(!CheckAuthed()) - { - return; - } - _context.CredentialIdentity = credentialIdentity; - CheckLock(() => ProvideCredential()); - } - - public override void PrepareInterfaceForExtensionConfiguration() - { - _context.Configuring = true; - if(!CheckAuthed()) - { - return; - } - CheckLock(() => PerformSegue("setupSegue", this)); - } - - public void CompleteRequest(string username = null, string password = null, string totp = null) - { - if((_context?.Configuring ?? true) && string.IsNullOrWhiteSpace(password)) - { - ExtensionContext?.CompleteExtensionConfigurationRequest(); - return; - } - - if(_context == null || string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - { - _googleAnalyticsService.TrackAutofillExtensionEvent("Canceled"); - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); - _googleAnalyticsService.Dispatch(() => - { - NSRunLoop.Main.BeginInvokeOnMainThread(() => - { - ExtensionContext?.CancelRequest(err); - }); - }); - return; - } - - if(!string.IsNullOrWhiteSpace(totp)) - { - UIPasteboard.General.String = totp; - } - - _googleAnalyticsService.TrackAutofillExtensionEvent("AutoFilled"); - var cred = new ASPasswordCredential(username, password); - _googleAnalyticsService.Dispatch(() => - { - NSRunLoop.Main.BeginInvokeOnMainThread(() => - { - ExtensionContext?.CompleteRequest(cred, null); - }); - }); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var listLoginController = navController.TopViewController as LoginListViewController; - var listSearchController = navController.TopViewController as LoginSearchViewController; - var fingerprintViewController = navController.TopViewController as LockFingerprintViewController; - var pinViewController = navController.TopViewController as LockPinViewController; - var passwordViewController = navController.TopViewController as LockPasswordViewController; - var setupViewController = navController.TopViewController as SetupViewController; - - if(listLoginController != null) - { - listLoginController.Context = _context; - listLoginController.CPViewController = this; - } - else if(listSearchController != null) - { - listSearchController.Context = _context; - listSearchController.CPViewController = this; - } - else if(fingerprintViewController != null) - { - fingerprintViewController.CPViewController = this; - } - else if(pinViewController != null) - { - pinViewController.CPViewController = this; - } - else if(passwordViewController != null) - { - passwordViewController.CPViewController = this; - } - else if(setupViewController != null) - { - setupViewController.CPViewController = this; - } - } - } - - public void DismissLockAndContinue() - { - DismissViewController(false, () => - { - if(_context.CredentialIdentity != null) - { - ProvideCredential(); - return; - } - if(_context.Configuring) - { - PerformSegue("setupSegue", this); - return; - } - if(_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) - { - PerformSegue("loginSearchSegue", this); - } - else - { - PerformSegue("loginListSegue", this); - } - }); - } - - private void ProvideCredential() - { - var cipherService = Resolver.Resolve(); - var cipher = cipherService.GetByIdAsync(_context.CredentialIdentity.RecordIdentifier).GetAwaiter().GetResult(); - if(cipher == null || cipher.Type != App.Enums.CipherType.Login) - { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null); - ExtensionContext.CancelRequest(err); - return; - } - - string totpCode = null; - if(!_settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) - { - var totpKey = cipher.Login.Totp?.Decrypt(cipher.OrganizationId); - if(!string.IsNullOrWhiteSpace(totpKey)) - { - totpCode = Crypto.Totp(totpKey); - } - } - - CompleteRequest(cipher.Login.Username?.Decrypt(cipher.OrganizationId), - cipher.Login.Password?.Decrypt(cipher.OrganizationId), totpCode); - } - - private void CheckLock(Action notLockedAction) - { - var lockService = Resolver.Resolve(); - var lockType = lockService.GetLockTypeAsync(false).GetAwaiter().GetResult(); - switch(lockType) - { - case App.Enums.LockType.Fingerprint: - PerformSegue("lockFingerprintSegue", this); - break; - case App.Enums.LockType.PIN: - PerformSegue("lockPinSegue", this); - break; - case App.Enums.LockType.Password: - PerformSegue("lockPasswordSegue", this); - break; - default: - notLockedAction(); - break; - } - } - - private bool CheckAuthed() - { - var authService = Resolver.Resolve(); - if(!authService.IsAuthenticated) - { - var alert = Dialogs.CreateAlert(null, AppResources.MustLogInMainAppAutofill, AppResources.Ok, (a) => - { - CompleteRequest(); - }); - PresentViewController(alert, true, null); - return false; - } - return true; - } - - private void SetIoc() - { - var container = new Container(); - - // Services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterInstance(new DeviceInfoService(true)); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Repositories - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Other - container.RegisterSingleton(CrossConnectivity.Current); - container.RegisterSingleton(CrossFingerprint.Current); - - var settings = new Settings("group.com.8bit.bitwarden"); - container.RegisterSingleton(settings); - - Resolver.ResetResolver(new SimpleInjectorResolver(container)); - } - - private void SetCulture() - { - var localizeService = Resolver.Resolve(); - var ci = localizeService.GetCurrentCultureInfo(); - AppResources.Culture = ci; - localizeService.SetLocale(ci); - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/CredentialProviderViewController.designer.cs b/src/iOS.Autofill/CredentialProviderViewController.designer.cs deleted file mode 100644 index 8cc45f3a4..000000000 --- a/src/iOS.Autofill/CredentialProviderViewController.designer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("CredentialProviderViewController")] - partial class CredentialProviderViewController - { - void ReleaseDesignerOutlets () - { - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/Entitlements.plist b/src/iOS.Autofill/Entitlements.plist deleted file mode 100644 index ef9236c02..000000000 --- a/src/iOS.Autofill/Entitlements.plist +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.developer.authentication-services.autofill-credential-provider - - com.apple.security.application-groups - - group.com.8bit.bitwarden - - keychain-access-groups - - $(AppIdentifierPrefix)com.8bit.bitwarden - - - diff --git a/src/iOS.Autofill/Info.plist b/src/iOS.Autofill/Info.plist deleted file mode 100644 index 2f4f0a368..000000000 --- a/src/iOS.Autofill/Info.plist +++ /dev/null @@ -1,87 +0,0 @@ - - - - - MinimumOSVersion - 12.0 - CFBundleDevelopmentRegion - en - CFBundleIdentifier - com.8bit.bitwarden.autofill - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - XPC! - CFBundleShortVersionString - 1.22.0 - CFBundleSignature - ???? - CFBundleVersion - 46 - NSExtension - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.authentication-services-credential-provider-ui - NSExtensionAttributes - - ASCredentialProviderExtensionShowsConfigurationUI - - - - UIDeviceFamily - - 1 - 2 - - UISupportedInterfaceOrientations - - ITSAppUsesNonExemptEncryption - - ITSEncryptionExportComplianceCode - ecf076d3-4824-4d7b-b716-2a9a47d7d296 - CFBundleName - Bitwarden Autofill - CFBundleDisplayName - Bitwarden - UIRequiredDeviceCapabilities - - arm64 - - - CFBundleLocalizations - - en - es - zh-Hans - zh-Hant - pt-PT - pt-BR - sv - sk - it - fi - fr - ro - id - hr - hu - nl - tr - uk - de - dk - cz - nb - ja - et - vi - pl - ko - fa - - NSFaceIDUsageDescription - Use Face ID to unlock your vault. - - diff --git a/src/iOS.Autofill/LockFingerprintViewController.cs b/src/iOS.Autofill/LockFingerprintViewController.cs deleted file mode 100644 index 1adc9a00a..000000000 --- a/src/iOS.Autofill/LockFingerprintViewController.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Autofill -{ - public partial class LockFingerprintViewController : Core.Controllers.LockFingerprintViewController - { - public LockFingerprintViewController(IntPtr handle) : base(handle) - { } - - public CredentialProviderViewController CPViewController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UIButton BaseUseButton => UseButton; - public override UIButton BaseFingerprintButton => FingerprintButton; - public override Action Success => () => CPViewController.DismissLockAndContinue(); - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - - partial void FingerprintButton_TouchUpInside(UIButton sender) - { - var task = CheckFingerprintAsync(); - } - } -} diff --git a/src/iOS.Autofill/LockFingerprintViewController.designer.cs b/src/iOS.Autofill/LockFingerprintViewController.designer.cs deleted file mode 100644 index 9df92b01b..000000000 --- a/src/iOS.Autofill/LockFingerprintViewController.designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LockFingerprintViewController")] - partial class LockFingerprintViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIButton FingerprintButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIButton UseButton { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("FingerprintButton_TouchUpInside:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void FingerprintButton_TouchUpInside (UIKit.UIButton sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (FingerprintButton != null) { - FingerprintButton.Dispose (); - FingerprintButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (UseButton != null) { - UseButton.Dispose (); - UseButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs deleted file mode 100644 index a5eb523f8..000000000 --- a/src/iOS.Autofill/LockPasswordViewController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Autofill -{ - public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController - { - public LockPasswordViewController(IntPtr handle) : base(handle) - { } - - public CredentialProviderViewController CPViewController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UIBarButtonItem BaseSubmitButton => SubmitButton; - public override Action Success => () => CPViewController.DismissLockAndContinue(); - - partial void SubmitButton_Activated(UIBarButtonItem sender) - { - CheckPassword(); - } - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - } -} diff --git a/src/iOS.Autofill/LockPasswordViewController.designer.cs b/src/iOS.Autofill/LockPasswordViewController.designer.cs deleted file mode 100644 index 3e6edf80a..000000000 --- a/src/iOS.Autofill/LockPasswordViewController.designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LockPasswordViewController")] - partial class LockPasswordViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UITableView MainTableView { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SubmitButton { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SubmitButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (MainTableView != null) { - MainTableView.Dispose (); - MainTableView = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (SubmitButton != null) { - SubmitButton.Dispose (); - SubmitButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/LockPinViewController.cs b/src/iOS.Autofill/LockPinViewController.cs deleted file mode 100644 index 18e6eacfc..000000000 --- a/src/iOS.Autofill/LockPinViewController.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Autofill -{ - public partial class LockPinViewController : Core.Controllers.LockPinViewController - { - public LockPinViewController(IntPtr handle) : base(handle) - { } - - public CredentialProviderViewController CPViewController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UILabel BasePinLabel => PinLabel; - public override UILabel BaseInstructionLabel => InstructionLabel; - public override UITextField BasePinTextField => PinTextField; - public override Action Success => () => CPViewController.DismissLockAndContinue(); - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - } -} diff --git a/src/iOS.Autofill/LockPinViewController.designer.cs b/src/iOS.Autofill/LockPinViewController.designer.cs deleted file mode 100644 index 9227edd6e..000000000 --- a/src/iOS.Autofill/LockPinViewController.designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LockPinViewController")] - partial class LockPinViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel InstructionLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel PinLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UITextField PinTextField { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (InstructionLabel != null) { - InstructionLabel.Dispose (); - InstructionLabel = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (PinLabel != null) { - PinLabel.Dispose (); - PinLabel = null; - } - - if (PinTextField != null) { - PinTextField.Dispose (); - PinTextField = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginAddViewController.cs b/src/iOS.Autofill/LoginAddViewController.cs deleted file mode 100644 index dade7eb12..000000000 --- a/src/iOS.Autofill/LoginAddViewController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Foundation; -using UIKit; - -namespace Bit.iOS.Autofill -{ - public partial class LoginAddViewController : Core.Controllers.LoginAddViewController - { - public LoginAddViewController(IntPtr handle) : base(handle) - { } - - public LoginListViewController LoginListController { get; set; } - public LoginSearchViewController LoginSearchController { get; set; } - - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelBarButton; - public override UIBarButtonItem BaseSaveButton => SaveBarButton; - - public override Action Success => () => - { - _googleAnalyticsService.TrackAutofillExtensionEvent("CreatedLogin"); - LoginListController?.DismissModal(); - LoginSearchController?.DismissModal(); - }; - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - DismissViewController(true, null); - } - - async partial void SaveBarButton_Activated(UIBarButtonItem sender) - { - await SaveAsync(); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var passwordGeneratorController = navController.TopViewController as PasswordGeneratorViewController; - if(passwordGeneratorController != null) - { - passwordGeneratorController.PasswordOptions = Context.PasswordOptions; - passwordGeneratorController.Parent = this; - } - } - } - } -} diff --git a/src/iOS.Autofill/LoginAddViewController.designer.cs b/src/iOS.Autofill/LoginAddViewController.designer.cs deleted file mode 100644 index f256f50ac..000000000 --- a/src/iOS.Autofill/LoginAddViewController.designer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LoginAddViewController")] - partial class LoginAddViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SaveBarButton { get; set; } - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SaveBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SaveBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (SaveBarButton != null) { - SaveBarButton.Dispose (); - SaveBarButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs deleted file mode 100644 index 01456e672..000000000 --- a/src/iOS.Autofill/LoginListViewController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using Bit.iOS.Autofill.Models; -using Foundation; -using UIKit; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.iOS.Core.Views; -using Bit.iOS.Autofill.Utilities; - -namespace Bit.iOS.Autofill -{ - public partial class LoginListViewController : ExtendedUITableViewController - { - public LoginListViewController(IntPtr handle) : base(handle) - { } - - public Context Context { get; set; } - public CredentialProviderViewController CPViewController { get; set; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public async override void ViewDidLoad() - { - base.ViewDidLoad(); - NavItem.Title = AppResources.Items; - CancelBarButton.Title = AppResources.Cancel; - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; - TableView.Source = new TableSource(this); - await ((TableSource)TableView.Source).LoadItemsAsync(); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - - partial void AddBarButton_Activated(UIBarButtonItem sender) - { - PerformSegue("loginAddSegue", this); - } - - partial void SearchBarButton_Activated(UIBarButtonItem sender) - { - PerformSegue("loginSearchFromListSegue", this); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var searchLoginController = navController.TopViewController as LoginSearchViewController; - var addLoginController = navController.TopViewController as LoginAddViewController; - if(addLoginController != null) - { - addLoginController.Context = Context; - addLoginController.LoginListController = this; - } - if(searchLoginController != null) - { - searchLoginController.Context = Context; - searchLoginController.CPViewController = CPViewController; - } - } - } - - public void DismissModal() - { - DismissViewController(true, async () => - { - await ((TableSource)TableView.Source).LoadItemsAsync(); - TableView.ReloadData(); - }); - } - - public class TableSource : ExtensionTableSource - { - private Context _context; - private LoginListViewController _controller; - - public TableSource(LoginListViewController controller) - : base(controller.Context, controller) - { - _context = controller.Context; - _controller = controller; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - AutofillHelpers.TableRowSelected(tableView, indexPath, this, - _controller.CPViewController, _controller, _settings, "loginAddSegue"); - } - } - } -} diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs deleted file mode 100644 index 6e1f5265a..000000000 --- a/src/iOS.Autofill/LoginListViewController.designer.cs +++ /dev/null @@ -1,59 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LoginListViewController")] - partial class LoginListViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem AddBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Action ("AddBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SearchBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (AddBarButton != null) { - AddBarButton.Dispose (); - AddBarButton = null; - } - - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginSearchViewController.cs b/src/iOS.Autofill/LoginSearchViewController.cs deleted file mode 100644 index 64f3c301f..000000000 --- a/src/iOS.Autofill/LoginSearchViewController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using Bit.iOS.Autofill.Models; -using Foundation; -using UIKit; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.iOS.Core.Views; -using Bit.iOS.Autofill.Utilities; - -namespace Bit.iOS.Autofill -{ - public partial class LoginSearchViewController : ExtendedUITableViewController - { - public LoginSearchViewController(IntPtr handle) : base(handle) - { } - - public Context Context { get; set; } - public CredentialProviderViewController CPViewController { get; set; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public async override void ViewDidLoad() - { - base.ViewDidLoad(); - NavItem.Title = AppResources.SearchVault; - CancelBarButton.Title = AppResources.Cancel; - SearchBar.Placeholder = AppResources.Search; - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; - TableView.Source = new TableSource(this); - SearchBar.Delegate = new ExtensionSearchDelegate(TableView); - await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - - partial void AddBarButton_Activated(UIBarButtonItem sender) - { - PerformSegue("loginAddFromSearchSegue", this); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var addLoginController = navController.TopViewController as LoginAddViewController; - if(addLoginController != null) - { - addLoginController.Context = Context; - addLoginController.LoginSearchController = this; - } - } - } - - public void DismissModal() - { - DismissViewController(true, async () => - { - await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); - TableView.ReloadData(); - }); - } - - public class TableSource : ExtensionTableSource - { - private Context _context; - private LoginSearchViewController _controller; - - public TableSource(LoginSearchViewController controller) - : base(controller.Context, controller) - { - _context = controller.Context; - _controller = controller; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - AutofillHelpers.TableRowSelected(tableView, indexPath, this, - _controller.CPViewController, _controller, _settings, "loginAddFromSearchSegue"); - } - } - } -} diff --git a/src/iOS.Autofill/LoginSearchViewController.designer.cs b/src/iOS.Autofill/LoginSearchViewController.designer.cs deleted file mode 100644 index 8694182b2..000000000 --- a/src/iOS.Autofill/LoginSearchViewController.designer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("LoginSearchViewController")] - partial class LoginSearchViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UISearchBar SearchBar { get; set; } - - [Action ("AddBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (SearchBar != null) { - SearchBar.Dispose (); - SearchBar = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/Main.cs b/src/iOS.Autofill/Main.cs deleted file mode 100644 index c7c778069..000000000 --- a/src/iOS.Autofill/Main.cs +++ /dev/null @@ -1,15 +0,0 @@ -using UIKit; - -namespace Bit.iOS.Autofill -{ - public class Application - { - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, "AppDelegate"); - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard deleted file mode 100644 index 4bb6873f3..000000000 --- a/src/iOS.Autofill/MainInterface.storyboard +++ /dev/null @@ -1,696 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs deleted file mode 100644 index c4a6ccf2b..000000000 --- a/src/iOS.Autofill/Models/Context.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AuthenticationServices; -using Bit.iOS.Core.Models; -using Foundation; - -namespace Bit.iOS.Autofill.Models -{ - public class Context : AppExtensionContext - { - public NSExtensionContext ExtContext { get; set; } - public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; } - public ASPasswordCredentialIdentity CredentialIdentity { get; set; } - public bool Configuring { get; set; } - } -} diff --git a/src/iOS.Autofill/PasswordGeneratorViewController.cs b/src/iOS.Autofill/PasswordGeneratorViewController.cs deleted file mode 100644 index 402cef12b..000000000 --- a/src/iOS.Autofill/PasswordGeneratorViewController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Autofill -{ - public partial class PasswordGeneratorViewController : Core.Controllers.PasswordGeneratorViewController - { - public PasswordGeneratorViewController(IntPtr handle) : base(handle, true) - { } - - public LoginAddViewController Parent { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelBarButton; - public override UIBarButtonItem BaseSelectBarButton => SelectBarButton; - public override UILabel BasePasswordLabel => PasswordLabel; - - partial void SelectBarButton_Activated(UIBarButtonItem sender) - { - GoogleAnalyticsService.TrackAutofillExtensionEvent("SelectedGeneratedPassword"); - DismissViewController(true, () => - { - Parent.PasswordCell.TextField.Text = PasswordLabel.Text; - }); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - DismissViewController(true, null); - } - } -} diff --git a/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs b/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs deleted file mode 100644 index cff6e5a65..000000000 --- a/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs +++ /dev/null @@ -1,82 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("PasswordGeneratorViewController")] - partial class PasswordGeneratorViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIView BaseView { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIView OptionsContainer { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel PasswordLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SelectBarButton { get; set; } - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SelectBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SelectBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (BaseView != null) { - BaseView.Dispose (); - BaseView = null; - } - - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (OptionsContainer != null) { - OptionsContainer.Dispose (); - OptionsContainer = null; - } - - if (PasswordLabel != null) { - PasswordLabel.Dispose (); - PasswordLabel = null; - } - - if (SelectBarButton != null) { - SelectBarButton.Dispose (); - SelectBarButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/Properties/AssemblyInfo.cs b/src/iOS.Autofill/Properties/AssemblyInfo.cs deleted file mode 100644 index a4f6227ac..000000000 --- a/src/iOS.Autofill/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("BitwardeniOSAutofill")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("8bit Solutions LLC")] -[assembly: AssemblyProduct("Bitwarden")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("32f5a2d6-f54d-4da1-ae26-0a980d48f421")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/iOS.Autofill/Resources/Icon.png b/src/iOS.Autofill/Resources/Icon.png deleted file mode 100644 index e4b594ad5..000000000 Binary files a/src/iOS.Autofill/Resources/Icon.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/Icon@2x.png b/src/iOS.Autofill/Resources/Icon@2x.png deleted file mode 100644 index 37369f4d3..000000000 Binary files a/src/iOS.Autofill/Resources/Icon@2x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/Icon@3x.png b/src/iOS.Autofill/Resources/Icon@3x.png deleted file mode 100644 index 0bfafae46..000000000 Binary files a/src/iOS.Autofill/Resources/Icon@3x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/check.png b/src/iOS.Autofill/Resources/check.png deleted file mode 100644 index f2d0c4a2b..000000000 Binary files a/src/iOS.Autofill/Resources/check.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/check@2x.png b/src/iOS.Autofill/Resources/check@2x.png deleted file mode 100644 index 9efd035c6..000000000 Binary files a/src/iOS.Autofill/Resources/check@2x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/check@3x.png b/src/iOS.Autofill/Resources/check@3x.png deleted file mode 100644 index 0e95759b4..000000000 Binary files a/src/iOS.Autofill/Resources/check@3x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/fingerprint.png b/src/iOS.Autofill/Resources/fingerprint.png deleted file mode 100644 index 754e39825..000000000 Binary files a/src/iOS.Autofill/Resources/fingerprint.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/fingerprint@2x.png b/src/iOS.Autofill/Resources/fingerprint@2x.png deleted file mode 100644 index 79547a076..000000000 Binary files a/src/iOS.Autofill/Resources/fingerprint@2x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/fingerprint@3x.png b/src/iOS.Autofill/Resources/fingerprint@3x.png deleted file mode 100644 index 236c8da73..000000000 Binary files a/src/iOS.Autofill/Resources/fingerprint@3x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/logo.png b/src/iOS.Autofill/Resources/logo.png deleted file mode 100644 index 33ced5b12..000000000 Binary files a/src/iOS.Autofill/Resources/logo.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/logo@2x.png b/src/iOS.Autofill/Resources/logo@2x.png deleted file mode 100644 index 2a0ba60b9..000000000 Binary files a/src/iOS.Autofill/Resources/logo@2x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/logo@3x.png b/src/iOS.Autofill/Resources/logo@3x.png deleted file mode 100644 index 904731676..000000000 Binary files a/src/iOS.Autofill/Resources/logo@3x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/smile.png b/src/iOS.Autofill/Resources/smile.png deleted file mode 100644 index 25f2f45b1..000000000 Binary files a/src/iOS.Autofill/Resources/smile.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/smile@2x.png b/src/iOS.Autofill/Resources/smile@2x.png deleted file mode 100644 index 71dd21e35..000000000 Binary files a/src/iOS.Autofill/Resources/smile@2x.png and /dev/null differ diff --git a/src/iOS.Autofill/Resources/smile@3x.png b/src/iOS.Autofill/Resources/smile@3x.png deleted file mode 100644 index 6e7189e69..000000000 Binary files a/src/iOS.Autofill/Resources/smile@3x.png and /dev/null differ diff --git a/src/iOS.Autofill/SetupViewController.cs b/src/iOS.Autofill/SetupViewController.cs deleted file mode 100644 index 789de5800..000000000 --- a/src/iOS.Autofill/SetupViewController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using UIKit; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.iOS.Core.Utilities; -using XLabs.Ioc; -using Bit.App.Abstractions; - -namespace Bit.iOS.Autofill -{ - public partial class SetupViewController : ExtendedUIViewController - { - public SetupViewController(IntPtr handle) : base(handle) - { } - - public CredentialProviderViewController CPViewController { get; set; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - var descriptor = UIFontDescriptor.PreferredBody; - DescriptionLabel.Text = $@"{AppResources.AutofillSetup} - -{AppResources.AutofillSetup2}"; - DescriptionLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize); - DescriptionLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - ActivatedLabel.Text = AppResources.AutofillActivated; - ActivatedLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize * 1.3f); - - BackButton.Title = AppResources.Back; - base.ViewDidLoad(); - - var tasks = ASHelpers.ReplaceAllIdentities(Resolver.Resolve()); - } - - partial void BackButton_Activated(UIBarButtonItem sender) - { - CPViewController.CompleteRequest(); - } - } -} diff --git a/src/iOS.Autofill/SetupViewController.designer.cs b/src/iOS.Autofill/SetupViewController.designer.cs deleted file mode 100644 index 7374b7e8f..000000000 --- a/src/iOS.Autofill/SetupViewController.designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Autofill -{ - [Register ("SetupViewController")] - partial class SetupViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel ActivatedLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem BackButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel DescriptionLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIImageView IconImage { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Action ("BackButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void BackButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (ActivatedLabel != null) { - ActivatedLabel.Dispose (); - ActivatedLabel = null; - } - - if (BackButton != null) { - BackButton.Dispose (); - BackButton = null; - } - - if (DescriptionLabel != null) { - DescriptionLabel.Dispose (); - DescriptionLabel = null; - } - - if (IconImage != null) { - IconImage.Dispose (); - IconImage = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs deleted file mode 100644 index b82bcc101..000000000 --- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Resources; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Core.Views; -using Foundation; -using Plugin.Settings.Abstractions; -using UIKit; - -namespace Bit.iOS.Autofill.Utilities -{ - public static class AutofillHelpers - { - public static void TableRowSelected(UITableView tableView, NSIndexPath indexPath, - ExtensionTableSource tableSource, CredentialProviderViewController cpViewController, - UITableViewController controller, ISettings settings, string loginAddSegue) - { - tableView.DeselectRow(indexPath, true); - tableView.EndEditing(true); - - if(tableSource.Items == null || tableSource.Items.Count() == 0) - { - controller.PerformSegue(loginAddSegue, tableSource); - return; - } - - var item = tableSource.Items.ElementAt(indexPath.Row); - if(item == null) - { - cpViewController.CompleteRequest(null); - return; - } - - if(!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password)) - { - string totp = null; - if(!settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) - { - totp = tableSource.GetTotp(item); - } - - cpViewController.CompleteRequest(item.Username, item.Password, totp); - } - else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) || - !string.IsNullOrWhiteSpace(item.Totp.Value)) - { - var sheet = Dialogs.CreateActionSheet(item.Name, controller); - if(!string.IsNullOrWhiteSpace(item.Username)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyUsername, UIAlertActionStyle.Default, a => - { - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = item.Username; - var alert = Dialogs.CreateMessageAlert(AppResources.CopyUsername); - controller.PresentViewController(alert, true, () => - { - controller.DismissViewController(true, null); - }); - })); - } - - if(!string.IsNullOrWhiteSpace(item.Password)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyPassword, UIAlertActionStyle.Default, a => - { - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = item.Password; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedPassword); - controller.PresentViewController(alert, true, () => - { - controller.DismissViewController(true, null); - }); - })); - } - - if(!string.IsNullOrWhiteSpace(item.Totp.Value)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a => - { - var totp = tableSource.GetTotp(item); - if(string.IsNullOrWhiteSpace(totp)) - { - return; - } - - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = totp; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp); - controller.PresentViewController(alert, true, () => - { - controller.DismissViewController(true, null); - }); - })); - } - - sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null)); - controller.PresentViewController(sheet, true, null); - } - else - { - var alert = Dialogs.CreateAlert(null, AppResources.NoUsernamePasswordConfigured, AppResources.Ok); - controller.PresentViewController(alert, true, null); - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj deleted file mode 100644 index 4c29600c9..000000000 --- a/src/iOS.Autofill/iOS.Autofill.csproj +++ /dev/null @@ -1,302 +0,0 @@ - - - Debug - iPhoneSimulator - {C2B7ADCA-5964-43C5-A7C8-D3B6F8023F4F} - {EE2C853D-36AF-4FDB-B1AD-8E90477E2198};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Exe - Bit.iOS.Autofill - BitwardeniOSAutofill - Resources - Properties - - - - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG - prompt - 4 - false - i386, x86_64 - None - True - Entitlements.plist - 12.0 - False - False - False - False - True - False - False - Default - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler - - - none - true - bin\iPhoneSimulator\Release - prompt - 4 - Full - i386, x86_64 - false - Entitlements.plist - 9.3 - False - False - False - False - False - False - True - Default - NSUrlSessionHandler - False - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - - - true - full - false - bin\iPhone\Debug - DEBUG - prompt - 4 - false - ARM64 - Entitlements.plist - iPhone Developer - True - - - - - None - False - False - False - False - False - True - False - False - --http-message-handler=NSUrlSessionHandler - - - Default - NSUrlSessionHandler - - - none - true - bin\iPhone\Release - prompt - 4 - Entitlements.plist - ARM64 - false - iPhone Developer - Full - False - False - False - False - False - False - True - False - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - - - none - True - bin\iPhone\Ad-Hoc - prompt - 4 - False - ARM64 - Entitlements.plist - True - Automatic:AdHoc - iPhone Distribution - Full - False - False - False - False - False - False - True - False - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - - - none - True - bin\iPhone\AppStore - prompt - 4 - False - ARM64 - Entitlements.plist - Automatic:AppStore - iPhone Distribution - Full - False - False - False - False - False - False - True - False - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - 10.2 - Default - NSUrlSessionHandler - - - 9.3 - Full - False - False - False - i386, x86_64 - False - False - False - True - Default - NSUrlSessionHandler - False - bin\iPhoneSimulator\Ad-Hoc - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - Entitlements.plist - - - 9.3 - Full - False - False - False - i386, x86_64 - False - False - False - True - Default - NSUrlSessionHandler - False - bin\iPhoneSimulator\AppStore - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSAutofill --linkskip=BitwardenApp --linkskip=SQLite-net - - - - - - - - - - - - - - - SetupViewController.cs - - - - - - - - - - CredentialProviderViewController.cs - - - - LockFingerprintViewController.cs - - - LockPasswordViewController.cs - - - LockPinViewController.cs - - - LoginAddViewController.cs - - - LoginListViewController.cs - - - PasswordGeneratorViewController.cs - - - - LoginSearchViewController.cs - - - - - - - - - - - - - - - - - - - - - - - - - - {8a279ee4-4537-4656-9c93-44945e594556} - App - - - {B2538ADA-B605-4D6F-ACD2-62A409680F84} - iOS.Core - False - False - - - - - 4.4.0 - - - 2.0.5782 - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/iOS.Core/Constants.cs b/src/iOS.Core/Constants.cs deleted file mode 100644 index 50941c0bb..000000000 --- a/src/iOS.Core/Constants.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Bit.iOS.Core -{ - public static class Constants - { - public const string AppExtensionVersionNumberKey = "version_number"; - public const string AppExtensionUrlStringKey = "url_string"; - public const string AppExtensionUsernameKey = "username"; - public const string AppExtensionPasswordKey = "password"; - public const string AppExtensionTotpKey = "totp"; - public const string AppExtensionTitleKey = "login_title"; - public const string AppExtensionNotesKey = "notes"; - public const string AppExtensionSectionTitleKey = "section_title"; - public const string AppExtensionFieldsKey = "fields"; - public const string AppExtensionReturnedFieldsKey = "returned_fields"; - public const string AppExtensionOldPasswordKey = "old_password"; - public const string AppExtensionPasswordGeneratorOptionsKey = "password_generator_options"; - public const string AppExtensionGeneratedPasswordMinLengthKey = "password_min_length"; - public const string AppExtensionGeneratedPasswordMaxLengthKey = "password_max_length"; - public const string AppExtensionGeneratedPasswordRequireDigitsKey = "password_require_digits"; - public const string AppExtensionGeneratedPasswordRequireSymbolsKey = "password_require_symbols"; - public const string AppExtensionGeneratedPasswordForbiddenCharactersKey = "password_forbidden_characters"; - public const string AppExtensionWebViewPageFillScript = "fillScript"; - public const string AppExtensionWebViewPageDetails = "pageDetails"; - - public const string UTTypeAppExtensionFindLoginAction = "org.appextension.find-login-action"; - public const string UTTypeAppExtensionSaveLoginAction = "org.appextension.save-login-action"; - public const string UTTypeAppExtensionChangePasswordAction = "org.appextension.change-password-action"; - public const string UTTypeAppExtensionFillWebViewAction = "org.appextension.fill-webview-action"; - public const string UTTypeAppExtensionFillBrowserAction = "org.appextension.fill-browser-action"; - public const string UTTypeAppExtensionSetup = "com.8bit.bitwarden.extension-setup"; - } -} diff --git a/src/iOS.Core/Controllers/ExtendedUITableViewController.cs b/src/iOS.Core/Controllers/ExtendedUITableViewController.cs deleted file mode 100644 index 26ebd1184..000000000 --- a/src/iOS.Core/Controllers/ExtendedUITableViewController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bit.App.Abstractions; -using System; -using UIKit; -using XLabs.Ioc; - -namespace Bit.iOS.Core.Controllers -{ - public class ExtendedUITableViewController : UITableViewController - { - public ExtendedUITableViewController(IntPtr handle) - : base(handle) - { } - - public override void ViewDidAppear(bool animated) - { - var googleAnalyticsService = Resolver.Resolve(); - googleAnalyticsService.TrackPage(GetType().Name); - base.ViewDidAppear(animated); - } - } -} diff --git a/src/iOS.Core/Controllers/ExtendedUIViewController.cs b/src/iOS.Core/Controllers/ExtendedUIViewController.cs deleted file mode 100644 index 49cde1e34..000000000 --- a/src/iOS.Core/Controllers/ExtendedUIViewController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using UIKit; -using Bit.App.Abstractions; -using XLabs.Ioc; - -namespace Bit.iOS.Core.Controllers -{ - public class ExtendedUIViewController : UIViewController - { - public ExtendedUIViewController(IntPtr handle) - : base(handle) - { } - - public override void ViewDidAppear(bool animated) - { - var googleAnalyticsService = Resolver.Resolve(); - googleAnalyticsService.TrackPage(GetType().Name); - base.ViewDidAppear(animated); - } - } -} diff --git a/src/iOS.Core/Controllers/LockFingerprintViewController.cs b/src/iOS.Core/Controllers/LockFingerprintViewController.cs deleted file mode 100644 index 43447d96d..000000000 --- a/src/iOS.Core/Controllers/LockFingerprintViewController.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using UIKit; -using XLabs.Ioc; -using Plugin.Fingerprint.Abstractions; -using System.Threading.Tasks; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.App.Abstractions; - -namespace Bit.iOS.Core.Controllers -{ - public abstract class LockFingerprintViewController : ExtendedUIViewController - { - private IAppSettingsService _appSettingsService; - private IFingerprint _fingerprint; - private IDeviceInfoService _deviceInfo; - - public LockFingerprintViewController(IntPtr handle) : base(handle) - { } - - public abstract UINavigationItem BaseNavItem { get; } - public abstract UIBarButtonItem BaseCancelButton { get; } - public abstract UIButton BaseUseButton { get; } - public abstract UIButton BaseFingerprintButton { get; } - public abstract Action Success { get; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - _appSettingsService = Resolver.Resolve(); - _fingerprint = Resolver.Resolve(); - _deviceInfo = Resolver.Resolve(); - - BaseNavItem.Title = _deviceInfo.HasFaceIdSupport ? AppResources.VerifyFaceID : AppResources.VerifyFingerprint; - BaseCancelButton.Title = AppResources.Cancel; - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - - BaseUseButton.SetTitle(_deviceInfo.HasFaceIdSupport ? AppResources.UseFaceIDToUnlock : - AppResources.UseFingerprintToUnlock, UIControlState.Normal); - var descriptor = UIFontDescriptor.PreferredBody; - BaseUseButton.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize); - BaseUseButton.BackgroundColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f); - BaseUseButton.TintColor = UIColor.White; - BaseUseButton.TouchUpInside += UseButton_TouchUpInside; - - BaseFingerprintButton.SetImage(new UIImage(_deviceInfo.HasFaceIdSupport ? "smile.png" : "fingerprint.png"), - UIControlState.Normal); - - base.ViewDidLoad(); - } - - private void UseButton_TouchUpInside(object sender, EventArgs e) - { - var task = CheckFingerprintAsync(); - } - - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - var task = CheckFingerprintAsync(); - } - - public async Task CheckFingerprintAsync() - { - var fingerprintRequest = new AuthenticationRequestConfiguration( - _deviceInfo.HasFaceIdSupport ? AppResources.FaceIDDirection : AppResources.FingerprintDirection) - { - AllowAlternativeAuthentication = true, - CancelTitle = AppResources.Cancel, - FallbackTitle = AppResources.LogOut - }; - var result = await _fingerprint.AuthenticateAsync(fingerprintRequest); - if(result.Authenticated) - { - _appSettingsService.Locked = false; - Success(); - } - } - } -} diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs deleted file mode 100644 index 0b5ec45e1..000000000 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using UIKit; -using XLabs.Ioc; -using Foundation; -using Bit.iOS.Core.Views; -using Bit.App.Resources; -using Bit.iOS.Core.Utilities; -using Bit.App.Abstractions; -using System.Linq; -using Bit.iOS.Core.Controllers; - -namespace Bit.iOS.Core.Controllers -{ - public abstract class LockPasswordViewController : ExtendedUITableViewController - { - private IAppSettingsService _appSettingsService; - private IAuthService _authService; - private ICryptoService _cryptoService; - - public LockPasswordViewController(IntPtr handle) : base(handle) - { } - - public abstract UINavigationItem BaseNavItem { get; } - public abstract UIBarButtonItem BaseCancelButton { get; } - public abstract UIBarButtonItem BaseSubmitButton { get; } - public abstract Action Success { get; } - - public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( - AppResources.MasterPassword, useLabelAsPlaceholder: true); - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - _appSettingsService = Resolver.Resolve(); - _authService = Resolver.Resolve(); - _cryptoService = Resolver.Resolve(); - - BaseNavItem.Title = AppResources.VerifyMasterPassword; - BaseCancelButton.Title = AppResources.Cancel; - BaseSubmitButton.Title = AppResources.Submit; - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - - var descriptor = UIFontDescriptor.PreferredBody; - - MasterPasswordCell.TextField.SecureTextEntry = true; - MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go; - MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) => - { - CheckPassword(); - return true; - }; - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 70; - TableView.Source = new TableSource(this); - TableView.AllowsSelection = true; - - base.ViewDidLoad(); - } - - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - MasterPasswordCell.TextField.BecomeFirstResponder(); - } - - protected void CheckPassword() - { - if(string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text)) - { - var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok); - PresentViewController(alert, true, null); - return; - } - - var key = _cryptoService.MakeKeyFromPassword(MasterPasswordCell.TextField.Text, _authService.Email, - _authService.Kdf, _authService.KdfIterations); - if(key.Key.SequenceEqual(_cryptoService.Key.Key)) - { - _appSettingsService.Locked = false; - MasterPasswordCell.TextField.ResignFirstResponder(); - Success(); - } - else - { - // TODO: keep track of invalid attempts and logout? - - var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, - string.Format(null, AppResources.InvalidMasterPassword), AppResources.Ok, (a) => - { - - MasterPasswordCell.TextField.Text = string.Empty; - MasterPasswordCell.TextField.BecomeFirstResponder(); - }); - - PresentViewController(alert, true, null); - } - } - - public class TableSource : UITableViewSource - { - private LockPasswordViewController _controller; - - public TableSource(LockPasswordViewController controller) - { - _controller = controller; - } - - public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) - { - if(indexPath.Section == 0) - { - if(indexPath.Row == 0) - { - return _controller.MasterPasswordCell; - } - } - - return new UITableViewCell(); - } - - public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) - { - return UITableView.AutomaticDimension; - } - - public override nint NumberOfSections(UITableView tableView) - { - return 1; - } - - public override nint RowsInSection(UITableView tableview, nint section) - { - if(section == 0) - { - return 1; - } - - return 0; - } - - public override nfloat GetHeightForHeader(UITableView tableView, nint section) - { - return UITableView.AutomaticDimension; - } - - public override string TitleForHeader(UITableView tableView, nint section) - { - return null; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - tableView.DeselectRow(indexPath, true); - tableView.EndEditing(true); - - var cell = tableView.CellAt(indexPath); - if(cell == null) - { - return; - } - - var selectableCell = cell as ISelectable; - if(selectableCell != null) - { - selectableCell.Select(); - } - } - } - } -} diff --git a/src/iOS.Core/Controllers/LockPinViewController.cs b/src/iOS.Core/Controllers/LockPinViewController.cs deleted file mode 100644 index 2211c69e6..000000000 --- a/src/iOS.Core/Controllers/LockPinViewController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using UIKit; -using XLabs.Ioc; -using Bit.App.Abstractions; -using Bit.iOS.Core.Utilities; -using Bit.App.Resources; -using System.Diagnostics; -using Bit.iOS.Core.Controllers; - -namespace Bit.iOS.Core.Controllers -{ - public abstract class LockPinViewController : ExtendedUIViewController - { - private IAppSettingsService _appSettingsService; - private IAuthService _authService; - - public LockPinViewController(IntPtr handle) : base(handle) - { } - - public abstract UINavigationItem BaseNavItem { get; } - public abstract UIBarButtonItem BaseCancelButton { get; } - public abstract UILabel BasePinLabel { get; } - public abstract UILabel BaseInstructionLabel { get; } - public abstract UITextField BasePinTextField { get; } - public abstract Action Success { get; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - _appSettingsService = Resolver.Resolve(); - _authService = Resolver.Resolve(); - - BaseNavItem.Title = AppResources.VerifyPIN; - BaseCancelButton.Title = AppResources.Cancel; - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - - var descriptor = UIFontDescriptor.PreferredBody; - BasePinLabel.Font = UIFont.FromName("Menlo-Regular", 35); - - BaseInstructionLabel.Text = AppResources.EnterPIN; - BaseInstructionLabel.LineBreakMode = UILineBreakMode.WordWrap; - BaseInstructionLabel.Lines = 0; - BaseInstructionLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize * 0.8f); - BaseInstructionLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - BasePinTextField.EditingChanged += PinTextField_EditingChanged; - - base.ViewDidLoad(); - } - - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - BasePinTextField.BecomeFirstResponder(); - } - - private void PinTextField_EditingChanged(object sender, EventArgs e) - { - SetLabelText(); - - if(BasePinTextField.Text.Length >= 4) - { - if(BasePinTextField.Text == _authService.PIN) - { - Debug.WriteLine("BW Log, Start Dismiss PIN controller."); - _appSettingsService.Locked = false; - BasePinTextField.ResignFirstResponder(); - Success(); - } - else - { - // TODO: keep track of invalid attempts and logout? - - var alert = Dialogs.CreateAlert(null, AppResources.InvalidPIN, AppResources.Ok, (a) => - { - BasePinTextField.Text = string.Empty; - SetLabelText(); - BasePinTextField.BecomeFirstResponder(); - }); - PresentViewController(alert, true, null); - } - } - } - - private void SetLabelText() - { - var newText = string.Empty; - for(int i = 0; i < 4; i++) - { - newText += BasePinTextField.Text.Length <= i ? "- " : "• "; - } - - BasePinLabel.Text = newText.TrimEnd(); - } - } -} diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs deleted file mode 100644 index 7076a88bd..000000000 --- a/src/iOS.Core/Controllers/LoginAddViewController.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Resources; -using Bit.iOS.Core.Views; -using Foundation; -using UIKit; -using XLabs.Ioc; -using Bit.App; -using Plugin.Connectivity.Abstractions; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Core.Models; -using System.Threading.Tasks; -using AuthenticationServices; - -namespace Bit.iOS.Core.Controllers -{ - public abstract class LoginAddViewController : ExtendedUITableViewController - { - private ICipherService _cipherService; - private IFolderService _folderService; - private IConnectivity _connectivity; - private IEnumerable _folders; - protected IGoogleAnalyticsService _googleAnalyticsService; - - public LoginAddViewController(IntPtr handle) : base(handle) - { - } - - public AppExtensionContext Context { get; set; } - public FormEntryTableViewCell NameCell { get; set; } = new FormEntryTableViewCell(AppResources.Name); - public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username); - public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password); - public UITableViewCell GeneratePasswordCell { get; set; } = new UITableViewCell(UITableViewCellStyle.Subtitle, "GeneratePasswordCell"); - public FormEntryTableViewCell UriCell { get; set; } = new FormEntryTableViewCell(AppResources.URI); - public SwitchTableViewCell FavoriteCell { get; set; } = new SwitchTableViewCell(AppResources.Favorite); - public FormEntryTableViewCell NotesCell { get; set; } = new FormEntryTableViewCell(useTextView: true, height: 180); - public PickerTableViewCell FolderCell { get; set; } = new PickerTableViewCell(AppResources.Folder); - - public abstract UINavigationItem BaseNavItem { get; } - public abstract UIBarButtonItem BaseCancelButton { get; } - public abstract UIBarButtonItem BaseSaveButton { get; } - public abstract Action Success { get; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - _cipherService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _folderService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - BaseNavItem.Title = AppResources.AddItem; - BaseCancelButton.Title = AppResources.Cancel; - BaseSaveButton.Title = AppResources.Save; - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - - NameCell.TextField.Text = Context?.Uri?.Host ?? string.Empty; - NameCell.TextField.ReturnKeyType = UIReturnKeyType.Next; - NameCell.TextField.ShouldReturn += (UITextField tf) => - { - UsernameCell.TextField.BecomeFirstResponder(); - return true; - }; - - UsernameCell.TextField.AutocapitalizationType = UITextAutocapitalizationType.None; - UsernameCell.TextField.AutocorrectionType = UITextAutocorrectionType.No; - UsernameCell.TextField.SpellCheckingType = UITextSpellCheckingType.No; - UsernameCell.TextField.ReturnKeyType = UIReturnKeyType.Next; - UsernameCell.TextField.ShouldReturn += (UITextField tf) => - { - PasswordCell.TextField.BecomeFirstResponder(); - return true; - }; - - PasswordCell.TextField.SecureTextEntry = true; - PasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Next; - PasswordCell.TextField.ShouldReturn += (UITextField tf) => - { - UriCell.TextField.BecomeFirstResponder(); - return true; - }; - - GeneratePasswordCell.TextLabel.Text = AppResources.GeneratePassword; - GeneratePasswordCell.Accessory = UITableViewCellAccessory.DisclosureIndicator; - - UriCell.TextField.Text = Context?.UrlString ?? string.Empty; - UriCell.TextField.KeyboardType = UIKeyboardType.Url; - UriCell.TextField.ReturnKeyType = UIReturnKeyType.Next; - UriCell.TextField.ShouldReturn += (UITextField tf) => - { - NotesCell.TextView.BecomeFirstResponder(); - return true; - }; - - _folders = _folderService.GetAllAsync().GetAwaiter().GetResult(); - var folderNames = _folders.Select(s => s.Name.Decrypt()).OrderBy(s => s).ToList(); - folderNames.Insert(0, AppResources.FolderNone); - FolderCell.Items = folderNames; - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 70; - TableView.Source = new TableSource(this); - TableView.AllowsSelection = true; - - base.ViewDidLoad(); - } - - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - } - - protected async Task SaveAsync() - { - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(string.IsNullOrWhiteSpace(PasswordCell.TextField.Text)) - { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Password), AppResources.Ok); - return; - } - - if(string.IsNullOrWhiteSpace(NameCell.TextField.Text)) - { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Name), AppResources.Ok); - return; - } - - var cipher = new Cipher - { - Name = string.IsNullOrWhiteSpace(NameCell.TextField.Text) ? null : NameCell.TextField.Text.Encrypt(), - Notes = string.IsNullOrWhiteSpace(NotesCell.TextView.Text) ? null : NotesCell.TextView.Text.Encrypt(), - Favorite = FavoriteCell.Switch.On, - FolderId = FolderCell.SelectedIndex == 0 ? null : _folders.ElementAtOrDefault(FolderCell.SelectedIndex - 1)?.Id, - Type = App.Enums.CipherType.Login, - Login = new Login - { - Uris = null, - Username = string.IsNullOrWhiteSpace(UsernameCell.TextField.Text) ? null : UsernameCell.TextField.Text.Encrypt(), - Password = string.IsNullOrWhiteSpace(PasswordCell.TextField.Text) ? null : PasswordCell.TextField.Text.Encrypt() - } - }; - - if(!string.IsNullOrWhiteSpace(UriCell.TextField.Text)) - { - cipher.Login.Uris = new List - { - new LoginUri - { - Uri = UriCell.TextField.Text.Encrypt() - } - }; - } - - var saveTask = _cipherService.SaveAsync(cipher); - var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); - PresentViewController(loadingAlert, true, null); - await saveTask; - - await loadingAlert.DismissViewControllerAsync(true); - if(saveTask.Result.Succeeded) - { - if (await ASHelpers.IdentitiesCanIncremental()) - { - var identity = await ASHelpers.GetCipherIdentityAsync(saveTask.Result.Result.Id, _cipherService); - if (identity != null) - { - await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - } - } - else - { - await ASHelpers.ReplaceAllIdentities(_cipherService); - } - Success(); - } - else if(saveTask.Result.Errors.Count() > 0) - { - DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Result.Errors.First().Message, AppResources.Ok); - } - else - { - DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - } - - public void DisplayAlert(string title, string message, string accept) - { - var alert = Dialogs.CreateAlert(title, message, accept); - PresentViewController(alert, true, null); - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, AppResources.Ok); - } - - public class TableSource : UITableViewSource - { - private LoginAddViewController _controller; - - public TableSource(LoginAddViewController controller) - { - _controller = controller; - } - - public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) - { - if(indexPath.Section == 0) - { - if(indexPath.Row == 0) - { - return _controller.NameCell; - } - else if(indexPath.Row == 1) - { - return _controller.UsernameCell; - } - else if(indexPath.Row == 2) - { - return _controller.PasswordCell; - } - else if(indexPath.Row == 3) - { - return _controller.GeneratePasswordCell; - } - } - else if(indexPath.Section == 1) - { - return _controller.UriCell; - } - else if(indexPath.Section == 2) - { - if(indexPath.Row == 0) - { - return _controller.FolderCell; - } - else if(indexPath.Row == 1) - { - return _controller.FavoriteCell; - } - } - else if(indexPath.Section == 3) - { - return _controller.NotesCell; - } - - return new UITableViewCell(); - } - - public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) - { - return UITableView.AutomaticDimension; - } - - public override nint NumberOfSections(UITableView tableView) - { - return 4; - } - - public override nint RowsInSection(UITableView tableview, nint section) - { - if(section == 0) - { - return 4; - } - else if(section == 1) - { - return 1; - } - else if(section == 2) - { - return 2; - } - else - { - return 1; - } - } - - public override nfloat GetHeightForHeader(UITableView tableView, nint section) - { - return section == 0 || section == 3 ? UITableView.AutomaticDimension : 0.00001f; - } - - public override string TitleForHeader(UITableView tableView, nint section) - { - if(section == 0) - { - return AppResources.ItemInformation; - } - else if(section == 3) - { - return AppResources.Notes; - } - - return string.Empty; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - tableView.DeselectRow(indexPath, true); - tableView.EndEditing(true); - - if(indexPath.Section == 0 && indexPath.Row == 3) - { - _controller.PerformSegue("passwordGeneratorSegue", this); - } - - var cell = tableView.CellAt(indexPath); - if(cell == null) - { - return; - } - - var selectableCell = cell as ISelectable; - if(selectableCell != null) - { - selectableCell.Select(); - } - } - } - } -} diff --git a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs deleted file mode 100644 index 089c7b447..000000000 --- a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Abstractions; -using Bit.iOS.Core.Views; -using Bit.iOS.Core.Models; -using Foundation; -using UIKit; -using XLabs.Ioc; -using Plugin.Settings.Abstractions; -using CoreGraphics; -using Bit.iOS.Core.Utilities; -using Bit.App.Resources; - -namespace Bit.iOS.Core.Controllers -{ - public abstract class PasswordGeneratorViewController : ExtendedUIViewController - { - private IPasswordGenerationService _passwordGenerationService; - private ISettings _settings; - private bool _isAutofill; - - public PasswordGeneratorViewController(IntPtr handle, bool autofill) : base(handle) - { - _isAutofill = autofill; - } - - protected IGoogleAnalyticsService GoogleAnalyticsService { get; private set; } - public UITableViewController OptionsTableViewController { get; set; } - public SwitchTableViewCell UppercaseCell { get; set; } = new SwitchTableViewCell("A-Z"); - public SwitchTableViewCell LowercaseCell { get; set; } = new SwitchTableViewCell("a-z"); - public SwitchTableViewCell NumbersCell { get; set; } = new SwitchTableViewCell("0-9"); - public SwitchTableViewCell SpecialCell { get; set; } = new SwitchTableViewCell("!@#$%^&*"); - public StepperTableViewCell MinNumbersCell { get; set; } = new StepperTableViewCell(AppResources.MinNumbers, 1, 0, 5, 1); - public StepperTableViewCell MinSpecialCell { get; set; } = new StepperTableViewCell(AppResources.MinSpecial, 1, 0, 5, 1); - public SliderTableViewCell LengthCell { get; set; } = new SliderTableViewCell(AppResources.Length, 10, 5, 64); - - public PasswordGenerationOptions PasswordOptions { get; set; } - public abstract UINavigationItem BaseNavItem { get; } - public abstract UIBarButtonItem BaseCancelButton { get; } - public abstract UIBarButtonItem BaseSelectBarButton { get; } - public abstract UILabel BasePasswordLabel { get; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - _passwordGenerationService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - GoogleAnalyticsService = Resolver.Resolve(); - - BaseNavItem.Title = AppResources.PasswordGenerator; - BaseCancelButton.Title = AppResources.Cancel; - BaseSelectBarButton.Title = AppResources.Select; - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - - var descriptor = UIFontDescriptor.PreferredBody; - BasePasswordLabel.Font = UIFont.FromName("Menlo-Regular", descriptor.PointSize * 1.3f); - BasePasswordLabel.LineBreakMode = UILineBreakMode.TailTruncation; - BasePasswordLabel.Lines = 1; - BasePasswordLabel.AdjustsFontSizeToFitWidth = false; - - var controller = ChildViewControllers.LastOrDefault(); - if(controller != null) - { - OptionsTableViewController = controller as UITableViewController; - } - - if(OptionsTableViewController != null) - { - OptionsTableViewController.TableView.RowHeight = UITableView.AutomaticDimension; - OptionsTableViewController.TableView.EstimatedRowHeight = 70; - OptionsTableViewController.TableView.Source = new TableSource(this); - OptionsTableViewController.TableView.AllowsSelection = true; - OptionsTableViewController.View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - } - - UppercaseCell.Switch.On = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorUppercase, true); - LowercaseCell.Switch.On = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorLowercase, true); - SpecialCell.Switch.On = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorSpecial, true); - NumbersCell.Switch.On = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorNumbers, true); - MinNumbersCell.Value = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorMinNumbers, 1); - MinSpecialCell.Value = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorMinSpecial, 1); - LengthCell.Value = _settings.GetValueOrDefault(App.Constants.PasswordGeneratorLength, 10); - - UppercaseCell.ValueChanged += Options_ValueChanged; - LowercaseCell.ValueChanged += Options_ValueChanged; - NumbersCell.ValueChanged += Options_ValueChanged; - SpecialCell.ValueChanged += Options_ValueChanged; - MinNumbersCell.ValueChanged += Options_ValueChanged; - MinSpecialCell.ValueChanged += Options_ValueChanged; - LengthCell.ValueChanged += Options_ValueChanged; - - // Adjust based on context password options - if(PasswordOptions != null) - { - if(PasswordOptions.RequireDigits) - { - NumbersCell.Switch.On = true; - NumbersCell.Switch.Enabled = false; - - if(MinNumbersCell.Value < 1) - { - MinNumbersCell.Value = 1; - } - - MinNumbersCell.Stepper.MinimumValue = 1; - } - - if(PasswordOptions.RequireSymbols) - { - SpecialCell.Switch.On = true; - SpecialCell.Switch.Enabled = false; - - if(MinSpecialCell.Value < 1) - { - MinSpecialCell.Value = 1; - } - - MinSpecialCell.Stepper.MinimumValue = 1; - } - - if(PasswordOptions.MinLength < PasswordOptions.MaxLength) - { - if(PasswordOptions.MinLength > 0 && PasswordOptions.MinLength > LengthCell.Slider.MinValue) - { - if(LengthCell.Value < PasswordOptions.MinLength) - { - LengthCell.Slider.Value = PasswordOptions.MinLength; - } - - LengthCell.Slider.MinValue = PasswordOptions.MinLength; - } - - if(PasswordOptions.MaxLength > 5 && PasswordOptions.MaxLength < LengthCell.Slider.MaxValue) - { - if(LengthCell.Value > PasswordOptions.MaxLength) - { - LengthCell.Slider.Value = PasswordOptions.MaxLength; - } - - LengthCell.Slider.MaxValue = PasswordOptions.MaxLength; - } - } - } - - GeneratePassword(); - if(_isAutofill) - { - GoogleAnalyticsService.TrackAutofillExtensionEvent("GeneratedPassword"); - } - else - { - GoogleAnalyticsService.TrackExtensionEvent("GeneratedPassword"); - } - base.ViewDidLoad(); - } - - private void Options_ValueChanged(object sender, EventArgs e) - { - if(InvalidState()) - { - LowercaseCell.Switch.On = true; - } - - GeneratePassword(); - } - - private bool InvalidState() - { - return !LowercaseCell.Switch.On && !UppercaseCell.Switch.On && !NumbersCell.Switch.On && !SpecialCell.Switch.On; - } - - private void GeneratePassword() - { - BasePasswordLabel.Text = _passwordGenerationService.GeneratePassword( - length: LengthCell.Value, - uppercase: UppercaseCell.Switch.On, - lowercase: LowercaseCell.Switch.On, - numbers: NumbersCell.Switch.On, - special: SpecialCell.Switch.On, - minSpecial: MinSpecialCell.Value, - minNumbers: MinNumbersCell.Value); - } - - public class TableSource : UITableViewSource - { - private PasswordGeneratorViewController _controller; - - public TableSource(PasswordGeneratorViewController controller) - { - _controller = controller; - } - - public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) - { - if(indexPath.Section == 0) - { - var cell = new UITableViewCell(); - cell.TextLabel.TextColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f); - if(indexPath.Row == 0) - { - cell.TextLabel.Text = AppResources.RegeneratePassword; - } - else if(indexPath.Row == 1) - { - cell.TextLabel.Text = AppResources.CopyPassword; - } - return cell; - } - - if(indexPath.Row == 0) - { - return _controller.LengthCell; - } - else if(indexPath.Row == 1) - { - return _controller.UppercaseCell; - } - else if(indexPath.Row == 2) - { - return _controller.LowercaseCell; - } - else if(indexPath.Row == 3) - { - return _controller.NumbersCell; - } - else if(indexPath.Row == 4) - { - return _controller.SpecialCell; - } - else if(indexPath.Row == 5) - { - return _controller.MinNumbersCell; - } - else if(indexPath.Row == 6) - { - return _controller.MinSpecialCell; - } - - return new UITableViewCell(); - } - - public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) - { - return UITableView.AutomaticDimension; - } - - public override nint NumberOfSections(UITableView tableView) - { - return 2; - } - - public override nint RowsInSection(UITableView tableview, nint section) - { - if(section == 0) - { - return 2; - } - - return 7; - } - - public override nfloat GetHeightForHeader(UITableView tableView, nint section) - { - if(section == 0) - { - return 0.00001f; - } - - return UITableView.AutomaticDimension; - } - - public override UIView GetViewForHeader(UITableView tableView, nint section) - { - if(section == 0) - { - return new UIView(CGRect.Empty) - { - Hidden = true - }; - } - - return null; - } - - public override string TitleForHeader(UITableView tableView, nint section) - { - if(section == 1) - { - return AppResources.Options; - } - - return null; - } - - public override string TitleForFooter(UITableView tableView, nint section) - { - if(section == 1) - { - return AppResources.OptionDefaults; - } - - return null; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - if(indexPath.Section == 0) - { - if(indexPath.Row == 0) - { - if(_controller._isAutofill) - { - _controller.GoogleAnalyticsService.TrackAutofillExtensionEvent("RegeneratedPassword"); - } - else - { - _controller.GoogleAnalyticsService.TrackExtensionEvent("RegeneratedPassword"); - } - _controller.GeneratePassword(); - } - else if(indexPath.Row == 1) - { - if(_controller._isAutofill) - { - _controller.GoogleAnalyticsService.TrackAutofillExtensionEvent("CopiedGeneratedPassword"); - } - else - { - _controller.GoogleAnalyticsService.TrackExtensionEvent("CopiedGeneratedPassword"); - } - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = _controller.BasePasswordLabel.Text; - var alert = Dialogs.CreateMessageAlert(AppResources.Copied); - _controller.PresentViewController(alert, true, () => - { - _controller.DismissViewController(true, null); - }); - } - } - - tableView.DeselectRow(indexPath, true); - tableView.EndEditing(true); - } - - public NSDate DateTimeToNSDate(DateTime date) - { - DateTime reference = TimeZone.CurrentTimeZone.ToLocalTime( - new DateTime(2001, 1, 1, 0, 0, 0)); - return NSDate.FromTimeIntervalSinceReferenceDate( - (date - reference).TotalSeconds); - } - } - } -} diff --git a/src/iOS.Core/HockeyAppCrashManagerDelegate.cs b/src/iOS.Core/HockeyAppCrashManagerDelegate.cs deleted file mode 100644 index 4016f5ce8..000000000 --- a/src/iOS.Core/HockeyAppCrashManagerDelegate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.App.Abstractions; -using HockeyApp.iOS; -using Newtonsoft.Json; - -namespace Bit.iOS.Core -{ - public class HockeyAppCrashManagerDelegate : BITCrashManagerDelegate - { - private readonly IAppIdService _appIdService; - private readonly IAuthService _authService; - - public HockeyAppCrashManagerDelegate( - IAppIdService appIdService, - IAuthService authService) - { - _appIdService = appIdService; - _authService = authService; - } - - public override string ApplicationLogForCrashManager(BITCrashManager crashManager) - { - var log = new - { - AppId = _appIdService.AppId, - UserId = _authService.UserId - }; - - return JsonConvert.SerializeObject(log, Formatting.Indented); - } - } -} diff --git a/src/iOS.Core/Models/AppExtensionContext.cs b/src/iOS.Core/Models/AppExtensionContext.cs deleted file mode 100644 index e55377fdc..000000000 --- a/src/iOS.Core/Models/AppExtensionContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; - -namespace Bit.iOS.Core.Models -{ - public class AppExtensionContext - { - private string _uriString; - - public Uri Uri - { - get - { - Uri uri; - if(string.IsNullOrWhiteSpace(UrlString) || !Uri.TryCreate(UrlString, UriKind.Absolute, out uri)) - { - return null; - } - - return uri; - } - } - public string UrlString - { - get - { - return _uriString; - } - set - { - _uriString = value; - if(string.IsNullOrWhiteSpace(_uriString)) - { - return; - } - - if(!_uriString.StartsWith(App.Constants.iOSAppProtocol) && _uriString.Contains(".")) - { - if(!_uriString.Contains("://") && !_uriString.Contains(" ")) - { - _uriString = string.Concat("http://", _uriString); - } - } - - if(!_uriString.StartsWith("http") && !_uriString.StartsWith(App.Constants.iOSAppProtocol)) - { - _uriString = string.Concat(App.Constants.iOSAppProtocol, _uriString); - } - } - } - public PasswordGenerationOptions PasswordOptions { get; set; } - } -} \ No newline at end of file diff --git a/src/iOS.Core/Models/CipherViewModel.cs b/src/iOS.Core/Models/CipherViewModel.cs deleted file mode 100644 index 869ddc28f..000000000 --- a/src/iOS.Core/Models/CipherViewModel.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Bit.App.Enums; -using Bit.App.Models; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Bit.iOS.Core.Models -{ - public class CipherViewModel - { - public CipherViewModel(Cipher cipher) - { - Id = cipher.Id; - Name = cipher.Name?.Decrypt(cipher.OrganizationId); - Username = cipher.Login?.Username?.Decrypt(cipher.OrganizationId); - Password = cipher.Login?.Password?.Decrypt(cipher.OrganizationId); - Uris = cipher.Login?.Uris?.Select(u => new LoginUriModel(u, cipher.OrganizationId)); - Totp = new Lazy(() => cipher.Login?.Totp?.Decrypt(cipher.OrganizationId)); - Fields = new Lazy>>(() => - { - if(!cipher.Fields?.Any() ?? true) - { - return null; - } - - var fields = new List>(); - foreach(var field in cipher.Fields) - { - fields.Add(new Tuple( - field.Name?.Decrypt(cipher.OrganizationId), - field.Value?.Decrypt(cipher.OrganizationId))); - } - return fields; - }); - } - - public string Id { get; set; } - public string Name { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public IEnumerable Uris { get; set; } - public Lazy Totp { get; set; } - public Lazy>> Fields { get; set; } - - public class LoginUriModel - { - public LoginUriModel(LoginUri data, string orgId) - { - Uri = data?.Uri?.Decrypt(orgId); - Match = data?.Match; - } - - public string Uri { get; set; } - public UriMatchType? Match { get; set; } - } - } -} diff --git a/src/iOS.Core/Models/PasswordGenerationOptions.cs b/src/iOS.Core/Models/PasswordGenerationOptions.cs deleted file mode 100644 index 4c2aa0c8b..000000000 --- a/src/iOS.Core/Models/PasswordGenerationOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Bit.iOS.Core.Models -{ - public class PasswordGenerationOptions - { - public int MinLength { get; set; } - public int MaxLength { get; set; } - public bool RequireDigits { get; set; } - public bool RequireSymbols { get; set; } - public string ForbiddenCharacters { get; set; } - } -} diff --git a/src/iOS.Core/Properties/AssemblyInfo.cs b/src/iOS.Core/Properties/AssemblyInfo.cs index 5fd5acd93..86b194178 100644 --- a/src/iOS.Core/Properties/AssemblyInfo.cs +++ b/src/iOS.Core/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -10,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("8bit Solutions LLC")] [assembly: AssemblyProduct("Bitwarden")] -[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -20,7 +19,7 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b2538ada-b605-4d6f-acd2-62a409680f84")] +[assembly: Guid("50c7b8c9-e664-45af-b88e-0c9b8b9c1be1")] // Version information for an assembly consists of the following four values: // diff --git a/src/iOS.Core/Services/AppInfoService.cs b/src/iOS.Core/Services/AppInfoService.cs deleted file mode 100644 index 090a57456..000000000 --- a/src/iOS.Core/Services/AppInfoService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Foundation; - -namespace Bit.iOS.Core.Services -{ - public class AppInfoService : IAppInfoService - { - public string Build => NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString(); - public string Version => NSBundle.MainBundle.InfoDictionary["CFBundleShortVersionString"].ToString(); - public bool AutofillAccessibilityServiceEnabled => false; - public bool AutofillServiceEnabled => false; - } -} diff --git a/src/iOS.Core/Services/CommonCryptoKeyDerivationService.cs b/src/iOS.Core/Services/CommonCryptoKeyDerivationService.cs deleted file mode 100644 index 8f296adab..000000000 --- a/src/iOS.Core/Services/CommonCryptoKeyDerivationService.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Bit.App.Abstractions; -using Foundation; -using System; -using System.Runtime.InteropServices; - -namespace Bit.iOS.Core.Services -{ - public class CommonCryptoKeyDerivationService : IKeyDerivationService - { - private const uint PBKDFAlgorithm = 2; // PBKDF2 - private const uint PseudoRandomAlgorithm = 3; // SHA256 - private const uint KeyLength = 32; // 256 bit - - public byte[] DeriveKey(byte[] password, byte[] salt, uint rounds) - { - var passwordData = NSData.FromArray(password); - var saltData = NSData.FromArray(salt); - - var keyData = new NSMutableData(); - keyData.Length = KeyLength; - var result = CCKeyCerivationPBKDF(PBKDFAlgorithm, passwordData.Bytes, passwordData.Length, saltData.Bytes, - saltData.Length, PseudoRandomAlgorithm, rounds, keyData.MutableBytes, keyData.Length); - - byte[] keyBytes = new byte[keyData.Length]; - Marshal.Copy(keyData.Bytes, keyBytes, 0, Convert.ToInt32(keyData.Length)); - return keyBytes; - } - - // ref: http://opensource.apple.com//source/CommonCrypto/CommonCrypto-55010/CommonCrypto/CommonKeyDerivation.h - [DllImport(ObjCRuntime.Constants.libSystemLibrary, EntryPoint = "CCKeyDerivationPBKDF")] - private extern static int CCKeyCerivationPBKDF(uint algorithm, IntPtr password, nuint passwordLen, - IntPtr salt, nuint saltLen, uint prf, nuint rounds, IntPtr derivedKey, nuint derivedKeyLength); - } -} diff --git a/src/iOS.Core/Services/CryptoPrimitiveService.cs b/src/iOS.Core/Services/CryptoPrimitiveService.cs new file mode 100644 index 000000000..93cb6d97c --- /dev/null +++ b/src/iOS.Core/Services/CryptoPrimitiveService.cs @@ -0,0 +1,47 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Foundation; +using System; +using System.Runtime.InteropServices; + +namespace Bit.iOS.Core.Services +{ + public class CryptoPrimitiveService : ICryptoPrimitiveService + { + private const uint PBKDFAlgorithm = 2; // kCCPBKDF2 + + public byte[] Pbkdf2(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) + { + uint keySize = 32; + uint pseudoRandomAlgorithm = 3; // kCCPRFHmacAlgSHA256 + if(algorithm == CryptoHashAlgorithm.Sha512) + { + keySize = 64; + pseudoRandomAlgorithm = 5; // kCCPRFHmacAlgSHA512 + } + else if(algorithm != CryptoHashAlgorithm.Sha256) + { + throw new ArgumentException("Unsupported PBKDF2 algorithm."); + } + + var keyData = new NSMutableData(); + keyData.Length = keySize; + + var passwordData = NSData.FromArray(password); + var saltData = NSData.FromArray(salt); + + var result = CCKeyCerivationPBKDF(PBKDFAlgorithm, passwordData.Bytes, passwordData.Length, saltData.Bytes, + saltData.Length, pseudoRandomAlgorithm, Convert.ToUInt32(iterations), keyData.MutableBytes, + keyData.Length); + + byte[] keyBytes = new byte[keyData.Length]; + Marshal.Copy(keyData.Bytes, keyBytes, 0, Convert.ToInt32(keyData.Length)); + return keyBytes; + } + + // ref: http://opensource.apple.com/source/CommonCrypto/CommonCrypto-55010/CommonCrypto/CommonKeyDerivation.h + [DllImport(ObjCRuntime.Constants.libSystemLibrary, EntryPoint = "CCKeyDerivationPBKDF")] + private extern static int CCKeyCerivationPBKDF(uint algorithm, IntPtr password, nuint passwordLen, + IntPtr salt, nuint saltLen, uint prf, nuint rounds, IntPtr derivedKey, nuint derivedKeyLength); + } +} diff --git a/src/iOS.Core/Services/DeviceInfoService.cs b/src/iOS.Core/Services/DeviceInfoService.cs deleted file mode 100644 index e51ff1f0a..000000000 --- a/src/iOS.Core/Services/DeviceInfoService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Bit.App.Abstractions; -using Foundation; -using LocalAuthentication; -using UIKit; - -namespace Bit.iOS.Core.Services -{ - public class DeviceInfoService : IDeviceInfoService - { - public DeviceInfoService() - : this(false) - { } - - public DeviceInfoService(bool isExtension) - { - IsExtension = isExtension; - } - - public string Type => Xamarin.Forms.Device.iOS; - public string Model => UIDevice.CurrentDevice.Model; - public int Version - { - get - { - var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.'); - if(versionParts.Length > 0 && int.TryParse(versionParts[0], out int version)) - { - return version; - } - - // unable to determine version - return -1; - } - } - public float Scale => (float)UIScreen.MainScreen.Scale; - public bool NfcEnabled => CoreNFC.NFCNdefReaderSession.ReadingAvailable; - public bool HasCamera => true; - public bool AutofillServiceSupported => false; - public bool HasFaceIdSupport - { - get - { - if(Version < 11) - { - return false; - } - - var context = new LAContext(); - if(!context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out NSError e)) - { - return false; - } - - return context.BiometryType == LABiometryType.FaceId; - } - } - public bool IsExtension { get; private set; } - } -} diff --git a/src/iOS.Core/Services/GoogleAnalyticsService.cs b/src/iOS.Core/Services/GoogleAnalyticsService.cs deleted file mode 100644 index a8eb843ec..000000000 --- a/src/iOS.Core/Services/GoogleAnalyticsService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Bit.App.Abstractions; -using Plugin.Settings.Abstractions; - -namespace Bit.iOS.Core.Services -{ - public class GoogleAnalyticsService : IGoogleAnalyticsService - { - public GoogleAnalyticsService( - IAppIdService appIdService, - ISettings settings) - {} - - public void TrackAppEvent(string eventName, string label = null) - { - } - - public void TrackExtensionEvent(string eventName, string label = null) - { - } - - public void TrackAutofillExtensionEvent(string eventName, string label = null) - { - } - - public void TrackEvent(string category, string eventName, string label = null) - { - } - - public void TrackException(string message, bool fatal) - { - } - - public void TrackPage(string pageName) - { - } - - public void Dispatch(Action completionHandler = null) - { - completionHandler?.Invoke(); - } - - public void SetAppOptOut(bool optOut) - { - } - } -} diff --git a/src/iOS.Core/Services/HttpService.cs b/src/iOS.Core/Services/HttpService.cs deleted file mode 100644 index e1c2c5128..000000000 --- a/src/iOS.Core/Services/HttpService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Bit.App; -using Bit.App.Abstractions; - -namespace Bit.iOS.Core.Services -{ - public class HttpService : IHttpService - { - public ApiHttpClient ApiClient => new ApiHttpClient(); - public IdentityHttpClient IdentityClient => new IdentityHttpClient(); - } -} diff --git a/src/iOS.Core/Services/KeyChainStorageService.cs b/src/iOS.Core/Services/KeyChainStorageService.cs index c646a43ff..ace993d47 100644 --- a/src/iOS.Core/Services/KeyChainStorageService.cs +++ b/src/iOS.Core/Services/KeyChainStorageService.cs @@ -1,92 +1,127 @@ using System; using System.Runtime.CompilerServices; -using Bit.App.Abstractions; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Abstractions; using Foundation; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Security; namespace Bit.iOS.Core.Services { - public class KeyChainStorageService : ISecureStorageService + public class KeyChainStorageService : IStorageService { - public void Store(string key, byte[] dataBytes) + private readonly string _keyFormat = "bwKeyChainStorage:{0}"; + private readonly string _service; + private readonly string _group; + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { - using(var data = NSData.FromArray(dataBytes)) - using(var newRecord = GetKeyRecord(key, data)) + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public KeyChainStorageService(string service, string group) + { + _service = service; + _group = group; + } + + public Task GetAsync(string key) + { + var formattedKey = string.Format(_keyFormat, key); + byte[] dataBytes = null; + using(var existingRecord = GetKeyRecord(formattedKey)) + using(var record = SecKeyChain.QueryAsRecord(existingRecord, out SecStatusCode resultCode)) { - Delete(key); + if(resultCode == SecStatusCode.ItemNotFound) + { + return Task.FromResult((T)(object)null); + } + + CheckError(resultCode); + dataBytes = record.Generic.ToArray(); + } + + var dataString = Encoding.UTF8.GetString(dataBytes); + if(typeof(T) == typeof(string)) + { + return Task.FromResult((T)(object)dataString); + } + else + { + return Task.FromResult(JsonConvert.DeserializeObject(dataString, _jsonSettings)); + } + } + + public async Task SaveAsync(string key, T obj) + { + if(obj == null) + { + await RemoveAsync(key); + return; + } + + string dataString = null; + if(typeof(T) == typeof(string)) + { + dataString = obj as string; + } + else + { + dataString = JsonConvert.SerializeObject(obj, _jsonSettings); + } + + var formattedKey = string.Format(_keyFormat, key); + var dataBytes = Encoding.UTF8.GetBytes(dataString); + using(var data = NSData.FromArray(dataBytes)) + using(var newRecord = GetKeyRecord(formattedKey, data)) + { + await RemoveAsync(formattedKey); CheckError(SecKeyChain.Add(newRecord)); } } - public byte[] Retrieve(string key) + public Task RemoveAsync(string key) { - SecStatusCode resultCode; - - using(var existingRecord = GetKeyRecord(key)) - using(var record = SecKeyChain.QueryAsRecord(existingRecord, out resultCode)) - { - if(resultCode == SecStatusCode.ItemNotFound) - { - return null; - } - - CheckError(resultCode); - return record.Generic.ToArray(); - } - } - - public void Delete(string key) - { - using(var record = GetExistingRecord(key)) + var formattedKey = string.Format(_keyFormat, key); + using(var record = GetExistingRecord(formattedKey)) { if(record != null) { CheckError(SecKeyChain.Remove(record)); } } + return Task.FromResult(0); } - public bool Contains(string key) + private SecRecord GetKeyRecord(string key, NSData data = null) { - using(var existingRecord = GetExistingRecord(key)) + var record = new SecRecord(SecKind.GenericPassword) { - return existingRecord != null; + Service = _service, + Account = key, + AccessGroup = _group + }; + if(data != null) + { + record.Generic = data; } + return record; } - private static void CheckError(SecStatusCode resultCode, [CallerMemberName] string caller = null) + private SecRecord GetExistingRecord(string key) + { + var existingRecord = GetKeyRecord(key); + SecKeyChain.QueryAsRecord(existingRecord, out SecStatusCode resultCode); + return resultCode == SecStatusCode.Success ? existingRecord : null; + } + + private void CheckError(SecStatusCode resultCode, [CallerMemberName] string caller = null) { if(resultCode != SecStatusCode.Success) { throw new Exception(string.Format("Failed to execute {0}. Result code: {1}", caller, resultCode)); } } - - private static SecRecord GetKeyRecord(string key, NSData data = null) - { - var record = new SecRecord(SecKind.GenericPassword) - { - Service = "com.8bit.bitwarden", - Account = key, - AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden" - }; - - if(data != null) - { - record.Generic = data; - } - - return record; - } - - private static SecRecord GetExistingRecord(string key) - { - var existingRecord = GetKeyRecord(key); - - SecStatusCode resultCode; - SecKeyChain.QueryAsRecord(existingRecord, out resultCode); - - return resultCode == SecStatusCode.Success ? existingRecord : null; - } } } diff --git a/src/iOS.Core/Services/LocalizeService.cs b/src/iOS.Core/Services/LocalizeService.cs index 28fab5b96..0c0bef664 100644 --- a/src/iOS.Core/Services/LocalizeService.cs +++ b/src/iOS.Core/Services/LocalizeService.cs @@ -1,21 +1,13 @@ using System; using System.Globalization; -using System.Threading; -using Foundation; using Bit.App.Abstractions; using Bit.App.Models; +using Foundation; namespace Bit.iOS.Core.Services { public class LocalizeService : ILocalizeService { - public void SetLocale(CultureInfo ci) - { - Thread.CurrentThread.CurrentCulture = ci; - Thread.CurrentThread.CurrentUICulture = ci; - Console.WriteLine("CurrentCulture set: " + ci.Name); - } - public CultureInfo GetCurrentCultureInfo() { var netLanguage = "en"; @@ -26,7 +18,7 @@ namespace Bit.iOS.Core.Services netLanguage = iOSToDotnetLanguage(pref); } - // this gets called a lot - try/catch can be expensive so consider caching or something + // This gets called a lot - try/catch can be expensive so consider caching or something CultureInfo ci = null; try { @@ -57,7 +49,6 @@ namespace Bit.iOS.Core.Services { Console.WriteLine("iOS Language:" + iOSLanguage); var netLanguage = iOSLanguage; - if(iOSLanguage.StartsWith("zh-Hant") || iOSLanguage.StartsWith("zh-HK")) { netLanguage = "zh-Hant"; @@ -68,7 +59,7 @@ namespace Bit.iOS.Core.Services } else { - //certain languages need to be converted to CultureInfo equivalent + // Certain languages need to be converted to CultureInfo equivalent switch(iOSLanguage) { case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture @@ -90,8 +81,8 @@ namespace Bit.iOS.Core.Services private string ToDotnetFallbackLanguage(PlatformCulture platCulture) { Console.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode); - var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually); - + // Use the first part of the identifier (two chars, usually); + var netLanguage = platCulture.LanguageCode; switch(platCulture.LanguageCode) { case "pt": @@ -103,9 +94,8 @@ namespace Bit.iOS.Core.Services // add more application-specific cases here (if required) // ONLY use cultures that have been tested and known to work } - Console.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)"); return netLanguage; } } -} +} \ No newline at end of file diff --git a/src/iOS.Core/Services/LogService.cs b/src/iOS.Core/Services/LogService.cs deleted file mode 100644 index e5c9b8131..000000000 --- a/src/iOS.Core/Services/LogService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Bit.App.Abstractions; - -namespace Bit.iOS.Core.Services -{ - public class LogService : ILogService - { - public void WriteLine(string message) - { - Console.WriteLine(message); - } - } -} diff --git a/src/iOS.Core/Services/NoopDeviceActionService.cs b/src/iOS.Core/Services/NoopDeviceActionService.cs deleted file mode 100644 index 36f35b20c..000000000 --- a/src/iOS.Core/Services/NoopDeviceActionService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using Bit.App.Abstractions; -using System.Threading.Tasks; -using Bit.App.Models.Page; -using Xamarin.Forms; - -namespace Bit.iOS.Core.Services -{ - public class NoopDeviceActionService : IDeviceActionService - { - public void Autofill(VaultListPageModel.Cipher cipher) - { - // do nothing - } - - public void Background() - { - // do nothing - } - - public bool CanOpenFile(string fileName) - { - return false; - } - - public void ClearCache() - { - // do nothing - } - - public void CloseAutofill() - { - // do nothing - } - - public void CopyToClipboard(string text) - { - // do nothing - } - - public void DismissKeyboard() - { - // do nothing - } - - public Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) - { - return Task.FromResult(null); - } - - public Task DisplayPromptAync(string title = null, string description = null, string text = null) - { - return Task.FromResult(null); - } - - public Task HideLoadingAsync() - { - return Task.FromResult(0); - } - - public Task LaunchAppAsync(string appName, Page page) - { - return Task.FromResult(0); - } - - public void OpenAccessibilitySettings() - { - // do nothing - } - - public void OpenAutofillSettings() - { - // do nothing - } - - public bool OpenFile(byte[] fileData, string id, string fileName) - { - return false; - } - - public void RateApp() - { - // do nothing - } - - public Task SelectFileAsync() - { - return Task.FromResult(0); - } - - public Task ShowLoadingAsync(string text) - { - return Task.FromResult(0); - } - - public void Toast(string text, bool longDuration = false) - { - // do nothing - } - } -} diff --git a/src/iOS.Core/Services/Settings.cs b/src/iOS.Core/Services/Settings.cs deleted file mode 100644 index 2d36bab01..000000000 --- a/src/iOS.Core/Services/Settings.cs +++ /dev/null @@ -1,495 +0,0 @@ -using System; -using Foundation; -#if __IOS__ -using UIKit; -#endif -using Plugin.Settings.Abstractions; - -namespace Bit.iOS.Core.Services -{ - /// - /// Main implementation for ISettings - /// - [Preserve(AllMembers = true)] - public class Settings : ISettings - { - private readonly object locker = new object(); - private readonly string _defaultsName; - - public Settings(string defaultsName) - { - _defaultsName = defaultsName; - } - - /// - /// Gets the current value or the default that you specify. - /// - /// Vaue of t (bool, int, float, long, string) - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - T GetValueOrDefaultInternal(string key, T defaultValue = default(T), string fileName = null) - { - lock(locker) - { - var defaults = GetUserDefaults(fileName); - - if(defaults[key] == null) - return defaultValue; - - Type typeOf = typeof(T); - if(typeOf.IsGenericType && typeOf.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - typeOf = Nullable.GetUnderlyingType(typeOf); - } - object value = null; - var typeCode = Type.GetTypeCode(typeOf); - switch(typeCode) - { - case TypeCode.Decimal: - var savedDecimal = defaults.StringForKey(key); - value = Convert.ToDecimal(savedDecimal, System.Globalization.CultureInfo.InvariantCulture); - break; - case TypeCode.Boolean: - value = defaults.BoolForKey(key); - break; - case TypeCode.Int64: - var savedInt64 = defaults.StringForKey(key); - value = Convert.ToInt64(savedInt64, System.Globalization.CultureInfo.InvariantCulture); - break; - case TypeCode.Double: - value = defaults.DoubleForKey(key); - break; - case TypeCode.String: - value = defaults.StringForKey(key); - break; - case TypeCode.Int32: - value = (Int32)defaults.IntForKey(key); - break; - case TypeCode.Single: - value = defaults.FloatForKey(key); - break; - - case TypeCode.DateTime: - var savedTime = defaults.StringForKey(key); - if(string.IsNullOrWhiteSpace(savedTime)) - { - value = defaultValue; - } - else - { - var ticks = Convert.ToInt64(savedTime, System.Globalization.CultureInfo.InvariantCulture); - if(ticks >= 0) - { - //Old value, stored before update to UTC values - value = new DateTime(ticks); - } - else - { - //New value, UTC - value = new DateTime(-ticks, DateTimeKind.Utc); - } - } - break; - default: - - if(defaultValue is Guid) - { - var outGuid = Guid.Empty; - var savedGuid = defaults.StringForKey(key); - if(string.IsNullOrWhiteSpace(savedGuid)) - { - value = outGuid; - } - else - { - Guid.TryParse(savedGuid, out outGuid); - value = outGuid; - } - } - else - { - throw new ArgumentException($"Value of type {typeCode} is not supported."); - } - - break; - } - - - return null != value ? (T)value : defaultValue; - } - } - - /// - /// Adds or updates a value - /// - /// key to update - /// value to set - /// Name of file for settings to be stored and retrieved - /// True if added or update and you need to save - bool AddOrUpdateValueInternal(string key, T value, string fileName = null) - { - if(value == null) - { - Remove(key, fileName); - return true; - } - - Type typeOf = typeof(T); - if(typeOf.IsGenericType && typeOf.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - typeOf = Nullable.GetUnderlyingType(typeOf); - } - var typeCode = Type.GetTypeCode(typeOf); - return AddOrUpdateValueCore(key, value, typeCode, fileName); - } - - bool AddOrUpdateValueCore(string key, object value, TypeCode typeCode, string fileName) - { - lock(locker) - { - var defaults = GetUserDefaults(fileName); - switch(typeCode) - { - case TypeCode.Decimal: - defaults.SetString(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture), key); - break; - case TypeCode.Boolean: - defaults.SetBool(Convert.ToBoolean(value), key); - break; - case TypeCode.Int64: - defaults.SetString(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture), key); - break; - case TypeCode.Double: - defaults.SetDouble(Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture), key); - break; - case TypeCode.String: - defaults.SetString(Convert.ToString(value), key); - break; - case TypeCode.Int32: - defaults.SetInt(Convert.ToInt32(value, System.Globalization.CultureInfo.InvariantCulture), key); - break; - case TypeCode.Single: - defaults.SetFloat(Convert.ToSingle(value, System.Globalization.CultureInfo.InvariantCulture), key); - break; - case TypeCode.DateTime: - defaults.SetString(Convert.ToString(-(Convert.ToDateTime(value)).ToUniversalTime().Ticks), key); - break; - default: - if(value is Guid) - { - if(value == null) - value = Guid.Empty; - - defaults.SetString(((Guid)value).ToString(), key); - } - else - { - throw new ArgumentException($"Value of type {typeCode} is not supported."); - } - break; - } - try - { - defaults.Synchronize(); - } - catch(Exception ex) - { - Console.WriteLine("Unable to save: " + key, " Message: " + ex.Message); - } - } - - - return true; - } - - /// - /// Removes a desired key from the settings - /// - /// Key for setting - /// Name of file for settings to be stored and retrieved - public void Remove(string key, string fileName = null) - { - lock(locker) - { - var defaults = GetUserDefaults(fileName); - try - { - if(defaults[key] != null) - { - defaults.RemoveObject(key); - defaults.Synchronize(); - } - } - catch(Exception ex) - { - Console.WriteLine("Unable to remove: " + key, " Message: " + ex.Message); - } - } - } - - /// - /// Clear all keys from settings - /// - /// Name of file for settings to be stored and retrieved - public void Clear(string fileName = null) - { - lock(locker) - { - var defaults = GetUserDefaults(fileName); - try - { - var items = defaults.ToDictionary(); - - foreach(var item in items.Keys) - { - if(item is NSString nsString) - defaults.RemoveObject(nsString); - } - defaults.Synchronize(); - } - catch(Exception ex) - { - Console.WriteLine("Unable to clear all defaults. Message: " + ex.Message); - } - } - } - - /// - /// Checks to see if the key has been added. - /// - /// Key to check - /// Name of file for settings to be stored and retrieved - /// True if contains key, else false - public bool Contains(string key, string fileName = null) - { - lock(locker) - { - var defaults = GetUserDefaults(fileName); - try - { - var setting = defaults[key]; - return setting != null; - } - catch(Exception ex) - { - Console.WriteLine("Unable to clear all defaults. Message: " + ex.Message); - } - - return false; - } - } - - NSUserDefaults GetUserDefaults(string fileName = null) - { - if(string.IsNullOrWhiteSpace(fileName) && !string.IsNullOrWhiteSpace(_defaultsName)) - { - return new NSUserDefaults(_defaultsName, NSUserDefaultsType.SuiteName); - } - - return string.IsNullOrWhiteSpace(fileName) ? - NSUserDefaults.StandardUserDefaults : - new NSUserDefaults(fileName, NSUserDefaultsType.SuiteName); - } - - - - #region GetValueOrDefault - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public decimal GetValueOrDefault(string key, decimal defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public bool GetValueOrDefault(string key, bool defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public long GetValueOrDefault(string key, long defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public string GetValueOrDefault(string key, string defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public int GetValueOrDefault(string key, int defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public float GetValueOrDefault(string key, float defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public DateTime GetValueOrDefault(string key, DateTime defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public Guid GetValueOrDefault(string key, Guid defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - /// - /// Gets the current value or the default that you specify. - /// - /// Key for settings - /// default value if not set - /// Name of file for settings to be stored and retrieved - /// Value or default - public double GetValueOrDefault(string key, double defaultValue, string fileName = null) => - GetValueOrDefaultInternal(key, defaultValue, fileName); - #endregion - - #region AddOrUpdateValue - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, decimal value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, bool value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, long value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, string value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, int value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, float value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, DateTime value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, Guid value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - /// - /// Adds or updates the value - /// - /// Key for settting - /// Value to set - /// Name of file for settings to be stored and retrieved - /// True of was added or updated and you need to save it. - public bool AddOrUpdateValue(string key, double value, string fileName = null) => - AddOrUpdateValueInternal(key, value, fileName); - - #endregion - - - /// - /// Attempts to open the app settings page. - /// - /// true if success, else false and not supported - public bool OpenAppSettings() - { -#if __IOS__ - //Opening settings only open in iOS 8+ - if(!UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) - return false; - - try - { - UIApplication.SharedApplication.OpenUrl(new NSUrl(UIApplication.OpenSettingsUrlString)); - return true; - } - catch - { - return false; - } -#else - return false; -#endif - } - - } - -} \ No newline at end of file diff --git a/src/iOS.Core/Services/SqlService.cs b/src/iOS.Core/Services/SqlService.cs deleted file mode 100644 index 570e834f9..000000000 --- a/src/iOS.Core/Services/SqlService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; -using Bit.App.Abstractions; -using Foundation; -using SQLite; - -namespace Bit.iOS.Core.Services -{ - public class SqlService : ISqlService - { - private SQLiteConnection _connection; - - public SQLiteConnection GetConnection() - { - if(_connection != null) - { - return _connection; - } - - var sqliteFilename = "bitwarden.db3"; - var fileManager = new NSFileManager(); - var appGroupContainer = fileManager.GetContainerUrl("group.com.8bit.bitwarden"); - var libraryPath = Path.Combine(appGroupContainer.Path, "Library"); // Library folder - var path = Path.Combine(libraryPath, sqliteFilename); - Console.WriteLine(path); - - _connection = new SQLiteConnection(path, - SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.SharedCache); - return _connection; - } - } -} diff --git a/src/iOS.Core/Utilities/ASHelpers.cs b/src/iOS.Core/Utilities/ASHelpers.cs deleted file mode 100644 index ac1692270..000000000 --- a/src/iOS.Core/Utilities/ASHelpers.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AuthenticationServices; -using Bit.App.Abstractions; -using Bit.App.Models; - -namespace Bit.iOS.Core.Utilities -{ - public static class ASHelpers - { - public static async Task ReplaceAllIdentities(ICipherService cipherService) - { - if (await AutofillEnabled()) - { - var identities = new List(); - var ciphers = await cipherService.GetAllAsync(); - foreach (var cipher in ciphers) - { - var identity = ToCredentialIdentity(cipher); - if (identity != null) - { - identities.Add(identity); - } - } - if (identities.Any()) - { - await ASCredentialIdentityStore.SharedStore?.ReplaceCredentialIdentitiesAsync(identities.ToArray()); - } - } - } - - public static async Task IdentitiesCanIncremental() - { - var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync(); - return state != null && state.Enabled && state.SupportsIncrementalUpdates; - } - - public static async Task AutofillEnabled() - { - var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync(); - return state != null && state.Enabled; - } - - public static async Task GetCipherIdentityAsync(string cipherId, ICipherService cipherService) - { - var cipher = await cipherService.GetByIdAsync(cipherId); - return ToCredentialIdentity(cipher); - } - - public static ASPasswordCredentialIdentity ToCredentialIdentity(Cipher cipher) - { - if (!cipher?.Login?.Uris?.Any() ?? true) - { - return null; - } - var uri = cipher.Login.Uris.FirstOrDefault()?.Uri?.Decrypt(cipher.OrganizationId); - if (string.IsNullOrWhiteSpace(uri)) - { - return null; - } - var username = cipher.Login.Username?.Decrypt(cipher.OrganizationId); - if (string.IsNullOrWhiteSpace(username)) - { - return null; - } - var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url); - return new ASPasswordCredentialIdentity(serviceId, username, cipher.Id); - } - } -} diff --git a/src/iOS.Core/Utilities/Dialogs.cs b/src/iOS.Core/Utilities/Dialogs.cs deleted file mode 100644 index 817013632..000000000 --- a/src/iOS.Core/Utilities/Dialogs.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Drawing; -using CoreGraphics; -using UIKit; - -namespace Bit.iOS.Core.Utilities -{ - public static class Dialogs - { - public static UIAlertController CreateLoadingAlert(string message) - { - var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50)); - loadingIndicator.HidesWhenStopped = true; - loadingIndicator.ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray; - loadingIndicator.StartAnimating(); - - var alert = UIAlertController.Create(null, message, UIAlertControllerStyle.Alert); - alert.View.TintColor = UIColor.Black; - alert.View.Add(loadingIndicator); - return alert; - } - - public static UIAlertController CreateMessageAlert(string message) - { - var alert = UIAlertController.Create(null, message, UIAlertControllerStyle.Alert); - alert.View.TintColor = UIColor.Black; - return alert; - } - - public static UIAlertController CreateAlert(string title, string message, string accept, Action acceptHandle = null) - { - var alert = UIAlertController.Create(title, message, UIAlertControllerStyle.Alert); - var oldFrame = alert.View.Frame; - alert.View.Frame = new RectangleF((float)oldFrame.X, (float)oldFrame.Y, (float)oldFrame.Width, (float)oldFrame.Height - 20); - alert.AddAction(UIAlertAction.Create(accept, UIAlertActionStyle.Default, acceptHandle)); - return alert; - } - - public static UIAlertController CreateActionSheet(string title, UIViewController controller) - { - var sheet = UIAlertController.Create(title, null, UIAlertControllerStyle.ActionSheet); - if(UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) - { - var x = controller.View.Bounds.Width / 2; - var y = controller.View.Bounds.Bottom; - var rect = new CGRect(x, y, 0, 0); - - sheet.PopoverPresentationController.SourceView = controller.View; - sheet.PopoverPresentationController.SourceRect = rect; - sheet.PopoverPresentationController.PermittedArrowDirections = UIPopoverArrowDirection.Unknown; - } - return sheet; - } - } -} diff --git a/src/iOS.Core/Views/ExtensionSearchDelegate.cs b/src/iOS.Core/Views/ExtensionSearchDelegate.cs deleted file mode 100644 index 317d55138..000000000 --- a/src/iOS.Core/Views/ExtensionSearchDelegate.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Foundation; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class ExtensionSearchDelegate : UISearchBarDelegate - { - private readonly UITableView _tableView; - private CancellationTokenSource _filterResultsCancellationTokenSource; - - public ExtensionSearchDelegate(UITableView tableView) - { - _tableView = tableView; - } - - public override void TextChanged(UISearchBar searchBar, string searchText) - { - var cts = new CancellationTokenSource(); - Task.Run(() => - { - NSRunLoop.Main.BeginInvokeOnMainThread(async () => - { - if(!string.IsNullOrWhiteSpace(searchText)) - { - await Task.Delay(300); - if(searchText != searchBar.Text) - { - return; - } - else - { - _filterResultsCancellationTokenSource?.Cancel(); - } - } - try - { - ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); - _tableView.ReloadData(); - } - catch(OperationCanceledException) { } - _filterResultsCancellationTokenSource = cts; - }); - }, cts.Token); - } - } -} \ No newline at end of file diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs deleted file mode 100644 index 4abcbc8a1..000000000 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Resources; -using Bit.App.Utilities; -using Bit.iOS.Core.Models; -using Foundation; -using Plugin.Settings.Abstractions; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using UIKit; -using XLabs.Ioc; - -namespace Bit.iOS.Core.Views -{ - public class ExtensionTableSource : UITableViewSource - { - private const string CellIdentifier = "TableCell"; - - private IEnumerable _allItems = new List(); - protected ICipherService _cipherService; - protected ISettings _settings; - private bool _accessPremium; - private AppExtensionContext _context; - private UIViewController _controller; - - public ExtensionTableSource(AppExtensionContext context, UIViewController controller) - { - _accessPremium = Helpers.CanAccessPremium(); - _cipherService = Resolver.Resolve(); - _settings = Resolver.Resolve(); - _context = context; - _controller = controller; - } - - public IEnumerable Items { get; private set; } - - public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null) - { - var combinedLogins = new List(); - - if(urlFilter) - { - var logins = await _cipherService.GetAllAsync(_context.UrlString); - if(logins?.Item1 != null) - { - combinedLogins.AddRange(logins.Item1); - } - if(logins?.Item2 != null) - { - combinedLogins.AddRange(logins.Item2); - } - } - else - { - var logins = await _cipherService.GetAllAsync(); - combinedLogins.AddRange(logins); - } - - _allItems = combinedLogins - .Where(c => c.Type == App.Enums.CipherType.Login) - .Select(s => new CipherViewModel(s)) - .OrderBy(s => s.Name) - .ThenBy(s => s.Username) - .ToList() ?? new List(); - FilterResults(searchFilter, new CancellationToken()); - } - - public void FilterResults(string searchFilter, CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - if(string.IsNullOrWhiteSpace(searchFilter)) - { - Items = _allItems.ToList(); - } - else - { - searchFilter = searchFilter.ToLower(); - Items = _allItems - .Where(s => s.Name?.ToLower().Contains(searchFilter) ?? false || - (s.Username?.ToLower().Contains(searchFilter) ?? false) || - (s.Uris?.FirstOrDefault()?.Uri?.ToLower().Contains(searchFilter) ?? false)) - .TakeWhile(s => !ct.IsCancellationRequested) - .ToArray(); - } - } - - public IEnumerable TableItems { get; set; } - - public override nint RowsInSection(UITableView tableview, nint section) - { - return Items == null || Items.Count() == 0 ? 1 : Items.Count(); - } - - public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) - { - if(Items == null || Items.Count() == 0) - { - var noDataCell = new UITableViewCell(UITableViewCellStyle.Default, "NoDataCell"); - noDataCell.TextLabel.Text = AppResources.NoItemsTap; - noDataCell.TextLabel.TextAlignment = UITextAlignment.Center; - noDataCell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; - noDataCell.TextLabel.Lines = 0; - return noDataCell; - } - - var cell = tableView.DequeueReusableCell(CellIdentifier); - - // if there are no cells to reuse, create a new one - if(cell == null) - { - Debug.WriteLine("BW Log, Make new cell for list."); - cell = new UITableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier); - cell.DetailTextLabel.TextColor = cell.DetailTextLabel.TintColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - } - return cell; - } - - public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) - { - if(Items == null || Items.Count() == 0 || cell == null) - { - return; - } - - var item = Items.ElementAt(indexPath.Row); - cell.TextLabel.Text = item.Name; - cell.DetailTextLabel.Text = item.Username; - } - - public string GetTotp(CipherViewModel item) - { - string totp = null; - if(_accessPremium) - { - if(item != null && !string.IsNullOrWhiteSpace(item.Totp.Value)) - { - totp = Crypto.Totp(item.Totp.Value); - } - } - - return totp; - } - } -} diff --git a/src/iOS.Core/Views/FormEntryTableViewCell.cs b/src/iOS.Core/Views/FormEntryTableViewCell.cs deleted file mode 100644 index 9088e7e0a..000000000 --- a/src/iOS.Core/Views/FormEntryTableViewCell.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class FormEntryTableViewCell : UITableViewCell, ISelectable - { - public FormEntryTableViewCell( - string labelName = null, - bool useTextView = false, - nfloat? height = null, - bool useLabelAsPlaceholder = false) - : base(UITableViewCellStyle.Default, nameof(FormEntryTableViewCell)) - { - var descriptor = UIFontDescriptor.PreferredBody; - var pointSize = descriptor.PointSize; - - if(labelName != null && !useLabelAsPlaceholder) - { - Label = new UILabel - { - Text = labelName, - TranslatesAutoresizingMaskIntoConstraints = false, - Font = UIFont.FromDescriptor(descriptor, 0.8f * pointSize), - TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f) - }; - - ContentView.Add(Label); - } - - if(useTextView) - { - TextView = new UITextView - { - TranslatesAutoresizingMaskIntoConstraints = false, - Font = UIFont.FromDescriptor(descriptor, pointSize) - }; - - ContentView.Add(TextView); - ContentView.AddConstraints(new NSLayoutConstraint[] { - NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextView, NSLayoutAttribute.Trailing, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextView, NSLayoutAttribute.Bottom, 1f, 10f) - }); - - if(labelName != null && !useLabelAsPlaceholder) - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f)); - } - else - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f)); - } - - if(height.HasValue) - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); - } - } - else - { - TextField = new UITextField - { - TranslatesAutoresizingMaskIntoConstraints = false, - BorderStyle = UITextBorderStyle.None, - Font = UIFont.FromDescriptor(descriptor, pointSize), - ClearButtonMode = UITextFieldViewMode.WhileEditing - }; - - if(useLabelAsPlaceholder) - { - TextField.Placeholder = labelName; - } - - ContentView.Add(TextField); - ContentView.AddConstraints(new NSLayoutConstraint[] { - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f) - }); - - if(labelName != null && !useLabelAsPlaceholder) - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f)); - } - else - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f)); - } - - if(height.HasValue) - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); - } - } - - if(labelName != null && !useLabelAsPlaceholder) - { - ContentView.AddConstraints(new NSLayoutConstraint[] { - NSLayoutConstraint.Create(Label, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), - NSLayoutConstraint.Create(Label, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Trailing, 1f, 15f) - }); - } - } - - public UILabel Label { get; set; } - public UITextField TextField { get; set; } - public UITextView TextView { get; set; } - - public void Select() - { - if(TextView != null) - { - TextView.BecomeFirstResponder(); - } - else if(TextField != null) - { - TextField.BecomeFirstResponder(); - } - } - } -} diff --git a/src/iOS.Core/Views/ISelectable.cs b/src/iOS.Core/Views/ISelectable.cs deleted file mode 100644 index 463d783fd..000000000 --- a/src/iOS.Core/Views/ISelectable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.iOS.Core.Views -{ - public interface ISelectable - { - void Select(); - } -} diff --git a/src/iOS.Core/Views/PickerTableViewCell.cs b/src/iOS.Core/Views/PickerTableViewCell.cs deleted file mode 100644 index 84094b752..000000000 --- a/src/iOS.Core/Views/PickerTableViewCell.cs +++ /dev/null @@ -1,195 +0,0 @@ -using CoreGraphics; -using System; -using System.Collections.Generic; -using System.Drawing; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class PickerTableViewCell : UITableViewCell, ISelectable - { - private List _items = new List(); - private int _selectedIndex = 0; - - public PickerTableViewCell( - string labelName, - nfloat? height = null) - : base(UITableViewCellStyle.Default, nameof(PickerTableViewCell)) - { - var descriptor = UIFontDescriptor.PreferredBody; - var pointSize = descriptor.PointSize; - - Label = new UILabel - { - Text = labelName, - TranslatesAutoresizingMaskIntoConstraints = false, - Font = UIFont.FromDescriptor(descriptor, 0.8f * pointSize), - TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f) - }; - - ContentView.Add(Label); - - TextField = new NoCaretField - { - BorderStyle = UITextBorderStyle.None, - TranslatesAutoresizingMaskIntoConstraints = false, - Font = UIFont.FromDescriptor(descriptor, pointSize) - }; - - var width = (float)UIScreen.MainScreen.Bounds.Width; - var toolbar = new UIToolbar(new RectangleF(0, 0, width, 44)) - { - BarStyle = UIBarStyle.Default, - Translucent = true - }; - var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace); - var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => - { - var s = (PickerSource)Picker.Model; - if(s.SelectedIndex == -1 && Items != null && Items.Count > 0) - { - UpdatePickerSelectedIndex(0); - } - TextField.Text = s.SelectedItem; - TextField.ResignFirstResponder(); - }); - - toolbar.SetItems(new[] { spacer, doneButton }, false); - - TextField.InputView = Picker; - TextField.InputAccessoryView = toolbar; - - ContentView.Add(TextField); - - ContentView.AddConstraints(new NSLayoutConstraint[] { - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 15f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f), - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f), - NSLayoutConstraint.Create(Label, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), - NSLayoutConstraint.Create(Label, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Trailing, 1f, 15f) - }); - - if(height.HasValue) - { - ContentView.AddConstraint( - NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); - } - - Picker.Model = new PickerSource(this); - } - - public UITextField TextField { get; set; } - public UILabel Label { get; set; } - public UIPickerView Picker { get; set; } = new UIPickerView(); - - public List Items - { - get { return _items; } - set - { - _items = value; - UpdatePicker(); - } - } - - public int SelectedIndex - { - get { return _selectedIndex; } - set - { - _selectedIndex = value; - UpdatePicker(); - } - } - - public string SelectedItem => TextField.Text; - - private void UpdatePicker() - { - TextField.Text = SelectedIndex == -1 || Items == null ? "" : Items[SelectedIndex]; - Picker.ReloadAllComponents(); - if(Items == null || Items.Count == 0) - { - return; - } - - UpdatePickerSelectedIndex(SelectedIndex); - } - - private void UpdatePickerFromModel(PickerSource s) - { - TextField.Text = s.SelectedItem; - _selectedIndex = s.SelectedIndex; - } - - private void UpdatePickerSelectedIndex(int formsIndex) - { - var source = (PickerSource)Picker.Model; - source.SelectedIndex = formsIndex; - source.SelectedItem = formsIndex >= 0 ? Items[formsIndex] : null; - Picker.Select(Math.Max(formsIndex, 0), 0, true); - } - - public void Select() - { - TextField?.BecomeFirstResponder(); - } - - private class NoCaretField : UITextField - { - public NoCaretField() : base(default(CGRect)) - { } - - public override CGRect GetCaretRectForPosition(UITextPosition position) - { - return default(CGRect); - } - } - - private class PickerSource : UIPickerViewModel - { - private readonly PickerTableViewCell _cell; - - public PickerSource(PickerTableViewCell cell) - { - _cell = cell; - } - - public int SelectedIndex { get; internal set; } - public string SelectedItem { get; internal set; } - - public override nint GetComponentCount(UIPickerView picker) - { - return 1; - } - - public override nint GetRowsInComponent(UIPickerView pickerView, nint component) - { - return _cell.Items != null ? _cell.Items.Count : 0; - } - - public override string GetTitle(UIPickerView picker, nint row, nint component) - { - return _cell.Items[(int)row]; - } - - public override void Selected(UIPickerView picker, nint row, nint component) - { - if(_cell.Items.Count == 0) - { - SelectedItem = null; - SelectedIndex = -1; - } - else - { - SelectedItem = _cell.Items[(int)row]; - SelectedIndex = (int)row; - } - - _cell.UpdatePickerFromModel(this); - } - } - } -} diff --git a/src/iOS.Core/Views/SliderTableViewCell.cs b/src/iOS.Core/Views/SliderTableViewCell.cs deleted file mode 100644 index 30231d2dd..000000000 --- a/src/iOS.Core/Views/SliderTableViewCell.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class SliderTableViewCell : UITableViewCell - { - private string _detailRightSpace = "\t"; - private int _value; - - public SliderTableViewCell(string labelName, int value, int min, int max) - : base(UITableViewCellStyle.Value1, nameof(SwitchTableViewCell)) - { - TextLabel.Text = labelName; - DetailTextLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - Slider = new UISlider - { - MinValue = min, - MaxValue = max, - TintColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f), - Frame = new CoreGraphics.CGRect(0, 0, 180, 30) - }; - Slider.ValueChanged += Slider_ValueChanged; - Value = value; - - AccessoryView = Slider; - } - - private void Slider_ValueChanged(object sender, EventArgs e) - { - var newValue = Convert.ToInt32(Math.Round(Slider.Value, 0)); - bool valueChanged = newValue != Value; - - Value = newValue; - - if(valueChanged) - { - ValueChanged?.Invoke(this, null); - } - } - - public UISlider Slider { get; set; } - public int Value - { - get { return _value; } - set - { - _value = value; - Slider.Value = value; - DetailTextLabel.Text = string.Concat(value.ToString(), _detailRightSpace); - } - } - public event EventHandler ValueChanged; - } -} diff --git a/src/iOS.Core/Views/StepperTableViewCell.cs b/src/iOS.Core/Views/StepperTableViewCell.cs deleted file mode 100644 index 02bdf8ce1..000000000 --- a/src/iOS.Core/Views/StepperTableViewCell.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class StepperTableViewCell : UITableViewCell - { - // Give some space to the right of the detail in between the spacer. - // This is a bit of a hack, but I did not see a way to specify a margin on the - // detaul DetailTextLabel or AccessoryView - private string _detailRightSpace = "\t"; - private int _value; - - public StepperTableViewCell(string labelName, int value, int min, int max, int increment) - : base(UITableViewCellStyle.Value1, nameof(SwitchTableViewCell)) - { - TextLabel.Text = labelName; - DetailTextLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - Stepper = new UIStepper - { - TintColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f), - MinimumValue = min, - MaximumValue = max - }; - Stepper.ValueChanged += Stepper_ValueChanged; - Value = value; - - AccessoryView = Stepper; - } - - private void Stepper_ValueChanged(object sender, EventArgs e) - { - Value = Convert.ToInt32(Stepper.Value); - ValueChanged?.Invoke(this, null); - } - - public UIStepper Stepper { get; private set; } - public int Value - { - get { return _value; } - set - { - _value = value; - Stepper.Value = value; - DetailTextLabel.Text = string.Concat(value.ToString(), _detailRightSpace); - } - } - public event EventHandler ValueChanged; - } -} diff --git a/src/iOS.Core/Views/SwitchTableViewCell.cs b/src/iOS.Core/Views/SwitchTableViewCell.cs deleted file mode 100644 index 9302f89e4..000000000 --- a/src/iOS.Core/Views/SwitchTableViewCell.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Core.Views -{ - public class SwitchTableViewCell : UITableViewCell - { - public SwitchTableViewCell(string labelName) - : base(UITableViewCellStyle.Default, nameof(SwitchTableViewCell)) - { - TextLabel.Text = labelName; - AccessoryView = Switch; - - Switch.ValueChanged += Switch_ValueChanged; - } - - private void Switch_ValueChanged(object sender, EventArgs e) - { - ValueChanged?.Invoke(this, null); - } - - public UISwitch Switch { get; set; } = new UISwitch(); - public event EventHandler ValueChanged; - } -} diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index 9972b60dc..ad5651b9d 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -3,8 +3,11 @@ Debug AnyCPU - {B2538ADA-B605-4D6F-ACD2-62A409680F84} + 8.0.30703 + 2.0 + {E71F3053-056C-4381-9638-048ED73BDFF6} {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {a52b8a63-bc84-4b47-910d-692533484892} Library Bit.iOS.Core Resources @@ -31,56 +34,36 @@ false + + ..\..\packages\Newtonsoft.Json.12.0.2\lib\netstandard2.0\Newtonsoft.Json.dll + + - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - {8a279ee4-4537-4656-9c93-44945e594556} + {ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c} App + + {4b8a8c41-9820-4341-974c-41e65b7f4366} + Core + + + + \ No newline at end of file diff --git a/src/iOS.Core/packages.config b/src/iOS.Core/packages.config new file mode 100644 index 000000000..c42689034 --- /dev/null +++ b/src/iOS.Core/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/iOS.Extension/AppDelegate.cs b/src/iOS.Extension/AppDelegate.cs deleted file mode 100644 index a8149c102..000000000 --- a/src/iOS.Extension/AppDelegate.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; - -using Foundation; -using UIKit; - -namespace Bit.iOS.Extension -{ - // The UIApplicationDelegate for the application. This class is responsible for launching the - // User Interface of the application, as well as listening (and optionally responding) to - // application events from iOS. - [Register("AppDelegate")] - public partial class AppDelegate : UIApplicationDelegate - { - // class-level declarations - - public override UIWindow Window - { - get; set; - } - - // This method is invoked when the application is about to move from active to inactive state. - // OpenGL applications should use this method to pause. - public override void OnResignActivation(UIApplication application) - { - } - - // This method should be used to release shared resources and it should store the application state. - // If your application supports background exection this method is called instead of WillTerminate - // when the user quits. - public override void DidEnterBackground(UIApplication application) - { - } - - // This method is called as part of the transiton from background to active state. - public override void WillEnterForeground(UIApplication application) - { - } - - // This method is called when the application is about to terminate. Save data, if needed. - public override void WillTerminate(UIApplication application) - { - } - } -} diff --git a/src/iOS.Extension/Entitlements.plist b/src/iOS.Extension/Entitlements.plist deleted file mode 100644 index b20900dcc..000000000 --- a/src/iOS.Extension/Entitlements.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.application-groups - - group.com.8bit.bitwarden - - keychain-access-groups - - $(AppIdentifierPrefix)com.8bit.bitwarden - - - diff --git a/src/iOS.Extension/Info.plist b/src/iOS.Extension/Info.plist deleted file mode 100644 index 11ca56447..000000000 --- a/src/iOS.Extension/Info.plist +++ /dev/null @@ -1,104 +0,0 @@ - - - - - MinimumOSVersion - 10.0 - CFBundleDevelopmentRegion - en - CFBundleIdentifier - com.8bit.bitwarden.find-login-action-extension - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - XPC! - CFBundleShortVersionString - 1.22.0 - CFBundleSignature - ???? - CFBundleVersion - 46 - NSExtension - - NSExtensionAttributes - - NSExtensionJavaScriptPreprocessingFile - extension - NSExtensionActivationRule - SUBQUERY ( - extensionItems, - $extensionItem, - SUBQUERY ( - $extensionItem.attachments, - $attachment, - ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.find-login-action" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.save-login-action" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.change-password-action" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.fill-webview-action" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.fill-browser-action" - || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.8bit.bitwarden.extension-setup" - ).@count == $extensionItem.attachments.@count - ).@count == 1 - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.ui-services - - UIDeviceFamily - - 1 - 2 - - UISupportedInterfaceOrientations - - ITSAppUsesNonExemptEncryption - - ITSEncryptionExportComplianceCode - ecf076d3-4824-4d7b-b716-2a9a47d7d296 - CFBundleName - Bitwarden Extension - CFBundleDisplayName - Bitwarden - UIRequiredDeviceCapabilities - - arm64 - - - CFBundleLocalizations - - en - es - zh-Hans - zh-Hant - pt-PT - pt-BR - sv - sk - it - fi - fr - ro - id - hr - hu - nl - tr - uk - de - dk - cz - nb - ja - et - vi - pl - ko - fa - - NSFaceIDUsageDescription - Use Face ID to unlock your vault. - - diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs deleted file mode 100644 index d33996598..000000000 --- a/src/iOS.Extension/LoadingViewController.cs +++ /dev/null @@ -1,530 +0,0 @@ -using System; -using System.Drawing; -using System.Diagnostics; -using Bit.App.Abstractions; -using Bit.App.Repositories; -using Bit.App.Services; -using Bit.iOS.Core.Services; -using Foundation; -using UIKit; -using XLabs.Ioc; -using Bit.iOS.Core; -using Newtonsoft.Json; -using Bit.iOS.Extension.Models; -using MobileCoreServices; -using Plugin.Settings.Abstractions; -using Plugin.Connectivity; -using Plugin.Fingerprint; -using Bit.iOS.Core.Utilities; -using Bit.App.Resources; -using Bit.iOS.Core.Controllers; -using SimpleInjector; -using XLabs.Ioc.SimpleInjectorContainer; -using System.Collections.Generic; -using Bit.iOS.Core.Models; - -namespace Bit.iOS.Extension -{ - public partial class LoadingViewController : ExtendedUIViewController - { - private Context _context = new Context(); - private bool _setupHockeyApp = false; - private readonly JsonSerializerSettings _jsonSettings = - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; - private IGoogleAnalyticsService _googleAnalyticsService; - - public LoadingViewController(IntPtr handle) : base(handle) - { } - - public override void ViewDidLoad() - { - SetIoc(); - SetCulture(); - - base.ViewDidLoad(); - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - _context.ExtContext = ExtensionContext; - _googleAnalyticsService = Resolver.Resolve(); - - if(!_setupHockeyApp) - { - var appIdService = Resolver.Resolve(); - var crashManagerDelegate = new HockeyAppCrashManagerDelegate(appIdService, Resolver.Resolve()); - var manager = HockeyApp.iOS.BITHockeyManager.SharedHockeyManager; - manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); - manager.CrashManager.CrashManagerStatus = HockeyApp.iOS.BITCrashManagerStatus.AutoSend; - manager.UserId = appIdService.AppId; - manager.StartManager(); - manager.Authenticator.AuthenticateInstallation(); - _setupHockeyApp = true; - } - - foreach(var item in ExtensionContext.InputItems) - { - var processed = false; - foreach(var itemProvider in item.Attachments) - { - if(ProcessWebUrlProvider(itemProvider) - || ProcessFindLoginProvider(itemProvider) - || ProcessFindLoginBrowserProvider(itemProvider, Constants.UTTypeAppExtensionFillBrowserAction) - || ProcessFindLoginBrowserProvider(itemProvider, Constants.UTTypeAppExtensionFillWebViewAction) - || ProcessSaveLoginProvider(itemProvider) - || ProcessChangePasswordProvider(itemProvider) - || ProcessExtensionSetupProvider(itemProvider)) - { - processed = true; - break; - } - } - - if(processed) - { - break; - } - } - } - - public override void ViewDidAppear(bool animated) - { - base.ViewDidAppear(animated); - - var authService = Resolver.Resolve(); - if(!authService.IsAuthenticated) - { - var alert = Dialogs.CreateAlert(null, AppResources.MustLogInMainApp, AppResources.Ok, (a) => - { - CompleteRequest(null); - }); - PresentViewController(alert, true, null); - return; - } - - if(_context.ProviderType == Constants.UTTypeAppExtensionSetup) - { - PerformSegue("setupSegue", this); - return; - } - - var lockService = Resolver.Resolve(); - var lockType = lockService.GetLockTypeAsync(false).GetAwaiter().GetResult(); - switch(lockType) - { - case App.Enums.LockType.Fingerprint: - PerformSegue("lockFingerprintSegue", this); - break; - case App.Enums.LockType.PIN: - PerformSegue("lockPinSegue", this); - break; - case App.Enums.LockType.Password: - PerformSegue("lockPasswordSegue", this); - break; - default: - ContinueOn(); - break; - } - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var listLoginController = navController.TopViewController as LoginListViewController; - var addLoginController = navController.TopViewController as LoginAddViewController; - var fingerprintViewController = navController.TopViewController as LockFingerprintViewController; - var pinViewController = navController.TopViewController as LockPinViewController; - var passwordViewController = navController.TopViewController as LockPasswordViewController; - var setupViewController = navController.TopViewController as SetupViewController; - - if(listLoginController != null) - { - listLoginController.Context = _context; - listLoginController.LoadingController = this; - } - else if(addLoginController != null) - { - addLoginController.Context = _context; - addLoginController.LoadingController = this; - } - else if(fingerprintViewController != null) - { - fingerprintViewController.LoadingController = this; - } - else if(pinViewController != null) - { - pinViewController.LoadingController = this; - } - else if(passwordViewController != null) - { - passwordViewController.LoadingController = this; - } - else if(setupViewController != null) - { - setupViewController.Context = _context; - setupViewController.LoadingController = this; - } - } - } - - public void DismissLockAndContinue() - { - Debug.WriteLine("BW Log, Dismissing lock controller."); - DismissViewController(false, () => - { - ContinueOn(); - }); - } - - private void ContinueOn() - { - Debug.WriteLine("BW Log, Segue to setup, login add or list."); - if(_context.ProviderType == Constants.UTTypeAppExtensionSaveLoginAction) - { - PerformSegue("newLoginSegue", this); - } - else if(_context.ProviderType == Constants.UTTypeAppExtensionSetup) - { - PerformSegue("setupSegue", this); - } - else - { - PerformSegue("loginListSegue", this); - } - } - - public void CompleteUsernamePasswordRequest(string username, string password, - List> fields, string totp) - { - NSDictionary itemData = null; - if(_context.ProviderType == UTType.PropertyList) - { - var fillScript = new FillScript(_context.Details, username, password, fields); - var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings); - var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson); - itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict); - } - else if(_context.ProviderType == Constants.UTTypeAppExtensionFindLoginAction) - { - itemData = new NSDictionary( - Constants.AppExtensionUsernameKey, username, - Constants.AppExtensionPasswordKey, password); - } - else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction - || _context.ProviderType == Constants.UTTypeAppExtensionFillWebViewAction) - { - var fillScript = new FillScript(_context.Details, username, password, fields); - var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings); - itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson); - } - else if(_context.ProviderType == Constants.UTTypeAppExtensionSaveLoginAction) - { - itemData = new NSDictionary( - Constants.AppExtensionUsernameKey, username, - Constants.AppExtensionPasswordKey, password); - } - else if(_context.ProviderType == Constants.UTTypeAppExtensionChangePasswordAction) - { - itemData = new NSDictionary( - Constants.AppExtensionPasswordKey, string.Empty, - Constants.AppExtensionOldPasswordKey, password); - } - - if(!string.IsNullOrWhiteSpace(totp)) - { - UIPasteboard.General.String = totp; - } - - CompleteRequest(itemData); - } - - public void CompleteRequest(NSDictionary itemData) - { - Debug.WriteLine("BW LOG, itemData: " + itemData); - - var resultsProvider = new NSItemProvider(itemData, UTType.PropertyList); - var resultsItem = new NSExtensionItem { Attachments = new NSItemProvider[] { resultsProvider } }; - var returningItems = new NSExtensionItem[] { resultsItem }; - - if(itemData != null) - { - _googleAnalyticsService.TrackExtensionEvent("AutoFilled", _context.ProviderType); - } - else - { - _googleAnalyticsService.TrackExtensionEvent("Closed", _context.ProviderType); - } - - _googleAnalyticsService.Dispatch(() => - { - NSRunLoop.Main.BeginInvokeOnMainThread(() => - { - Resolver.ResetResolver(); - ExtensionContext?.CompleteRequest(returningItems, null); - }); - }); - } - - private void SetIoc() - { - var container = new Container(); - - // Services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterInstance(new DeviceInfoService(true)); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Repositories - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Other - container.RegisterSingleton(CrossConnectivity.Current); - container.RegisterSingleton(CrossFingerprint.Current); - - var settings = new Settings("group.com.8bit.bitwarden"); - container.RegisterSingleton(settings); - - Resolver.ResetResolver(new SimpleInjectorResolver(container)); - } - - private void SetCulture() - { - var localizeService = Resolver.Resolve(); - var ci = localizeService.GetCurrentCultureInfo(); - AppResources.Culture = ci; - localizeService.SetLocale(ci); - } - - private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action dictAction, - Action urlAction = null) - { - if(!itemProvider.HasItemConformingTo(type)) - { - return false; - } - - itemProvider.LoadItem(type, null, (NSObject list, NSError error) => - { - if(list == null) - { - return; - } - - _context.ProviderType = type; - - var dict = list as NSDictionary; - if(dict != null && dictAction != null) - { - dictAction(dict); - } - else if(list is NSUrl && urlAction != null) - { - var url = list as NSUrl; - urlAction(url); - } - else - { - throw new Exception("Cannot parse list for action. List is " + - (list?.GetType().ToString() ?? "null")); - } - - _googleAnalyticsService.TrackExtensionEvent("ProcessItemProvider", type); - - Debug.WriteLine("BW LOG, ProviderType: " + _context.ProviderType); - Debug.WriteLine("BW LOG, Url: " + _context.UrlString); - Debug.WriteLine("BW LOG, Title: " + _context.LoginTitle); - Debug.WriteLine("BW LOG, Username: " + _context.Username); - Debug.WriteLine("BW LOG, Password: " + _context.Password); - Debug.WriteLine("BW LOG, Old Password: " + _context.OldPassword); - Debug.WriteLine("BW LOG, Notes: " + _context.Notes); - Debug.WriteLine("BW LOG, Details: " + _context.Details); - - if(_context.PasswordOptions != null) - { - Debug.WriteLine("BW LOG, PasswordOptions Min Length: " + _context.PasswordOptions.MinLength); - Debug.WriteLine("BW LOG, PasswordOptions Max Length: " + _context.PasswordOptions.MaxLength); - Debug.WriteLine("BW LOG, PasswordOptions Require Digits: " + _context.PasswordOptions.RequireDigits); - Debug.WriteLine("BW LOG, PasswordOptions Require Symbols: " + _context.PasswordOptions.RequireSymbols); - Debug.WriteLine("BW LOG, PasswordOptions Forbidden Chars: " + _context.PasswordOptions.ForbiddenCharacters); - } - }); - - return true; - } - - private bool ProcessWebUrlProvider(NSItemProvider itemProvider) - { - return ProcessItemProvider(itemProvider, UTType.PropertyList, (dict) => - { - var result = dict[NSJavaScriptExtension.PreprocessingResultsKey]; - if(result == null) - { - return; - } - - _context.UrlString = result.ValueForKey(new NSString(Constants.AppExtensionUrlStringKey)) as NSString; - var jsonStr = result.ValueForKey(new NSString(Constants.AppExtensionWebViewPageDetails)) as NSString; - _context.Details = DeserializeString(jsonStr); - }); - } - - private bool ProcessFindLoginProvider(NSItemProvider itemProvider) - { - return ProcessItemProvider(itemProvider, Constants.UTTypeAppExtensionFindLoginAction, (dict) => - { - var version = dict[Constants.AppExtensionVersionNumberKey] as NSNumber; - var url = dict[Constants.AppExtensionUrlStringKey] as NSString; - - if(url != null) - { - _context.UrlString = url; - } - }); - } - - private bool ProcessFindLoginBrowserProvider(NSItemProvider itemProvider, string action) - { - return ProcessItemProvider(itemProvider, action, (dict) => - { - var version = dict[Constants.AppExtensionVersionNumberKey] as NSNumber; - var url = dict[Constants.AppExtensionUrlStringKey] as NSString; - if(url != null) - { - _context.UrlString = url; - } - - _context.Details = DeserializeDictionary(dict[Constants.AppExtensionWebViewPageDetails] as NSDictionary); - }, (url) => - { - if(url != null) - { - _context.UrlString = url.AbsoluteString; - } - }); - } - - private bool ProcessSaveLoginProvider(NSItemProvider itemProvider) - { - return ProcessItemProvider(itemProvider, Constants.UTTypeAppExtensionSaveLoginAction, (dict) => - { - var version = dict[Constants.AppExtensionVersionNumberKey] as NSNumber; - var url = dict[Constants.AppExtensionUrlStringKey] as NSString; - var title = dict[Constants.AppExtensionTitleKey] as NSString; - var sectionTitle = dict[Constants.AppExtensionSectionTitleKey] as NSString; - var username = dict[Constants.AppExtensionUsernameKey] as NSString; - var password = dict[Constants.AppExtensionPasswordKey] as NSString; - var notes = dict[Constants.AppExtensionNotesKey] as NSString; - var fields = dict[Constants.AppExtensionFieldsKey] as NSDictionary; - - if(url != null) - { - _context.UrlString = url; - } - - _context.LoginTitle = title; - _context.Username = username; - _context.Password = password; - _context.Notes = notes; - _context.PasswordOptions = DeserializeDictionary(dict[Constants.AppExtensionPasswordGeneratorOptionsKey] as NSDictionary); - }); - } - - private bool ProcessChangePasswordProvider(NSItemProvider itemProvider) - { - return ProcessItemProvider(itemProvider, Constants.UTTypeAppExtensionChangePasswordAction, (dict) => - { - var version = dict[Constants.AppExtensionVersionNumberKey] as NSNumber; - var url = dict[Constants.AppExtensionUrlStringKey] as NSString; - var title = dict[Constants.AppExtensionTitleKey] as NSString; - var sectionTitle = dict[Constants.AppExtensionSectionTitleKey] as NSString; - var username = dict[Constants.AppExtensionUsernameKey] as NSString; - var password = dict[Constants.AppExtensionPasswordKey] as NSString; - var oldPassword = dict[Constants.AppExtensionOldPasswordKey] as NSString; - var notes = dict[Constants.AppExtensionNotesKey] as NSString; - var fields = dict[Constants.AppExtensionFieldsKey] as NSDictionary; - - if(url != null) - { - _context.UrlString = url; - } - - _context.LoginTitle = title; - _context.Username = username; - _context.Password = password; - _context.OldPassword = oldPassword; - _context.Notes = notes; - _context.PasswordOptions = DeserializeDictionary(dict[Constants.AppExtensionPasswordGeneratorOptionsKey] as NSDictionary); - }); - } - - private bool ProcessExtensionSetupProvider(NSItemProvider itemProvider) - { - if(itemProvider.HasItemConformingTo(Constants.UTTypeAppExtensionSetup)) - { - _context.ProviderType = Constants.UTTypeAppExtensionSetup; - return true; - } - - return false; - } - - private T DeserializeDictionary(NSDictionary dict) - { - if(dict != null) - { - NSError jsonError; - var jsonData = NSJsonSerialization.Serialize(dict, NSJsonWritingOptions.PrettyPrinted, out jsonError); - if(jsonData != null) - { - var jsonString = new NSString(jsonData, NSStringEncoding.UTF8); - return DeserializeString(jsonString); - } - } - - return default(T); - } - - private T DeserializeString(NSString jsonString) - { - if(jsonString != null) - { - var convertedObject = JsonConvert.DeserializeObject(jsonString.ToString()); - return convertedObject; - } - - return default(T); - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LoadingViewController.designer.cs b/src/iOS.Extension/LoadingViewController.designer.cs deleted file mode 100644 index 5d55235f4..000000000 --- a/src/iOS.Extension/LoadingViewController.designer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LoadingViewController")] - partial class LoadingViewController - { - void ReleaseDesignerOutlets () - { - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LockFingerprintViewController.cs b/src/iOS.Extension/LockFingerprintViewController.cs deleted file mode 100644 index a5a81466a..000000000 --- a/src/iOS.Extension/LockFingerprintViewController.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LockFingerprintViewController : Core.Controllers.LockFingerprintViewController - { - public LockFingerprintViewController(IntPtr handle) : base(handle) - { } - - public LoadingViewController LoadingController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UIButton BaseUseButton => UseButton; - public override UIButton BaseFingerprintButton => FingerprintButton; - public override Action Success => () => LoadingController.DismissLockAndContinue(); - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - LoadingController.CompleteRequest(null); - } - - partial void FingerprintButton_TouchUpInside(UIButton sender) - { - var task = CheckFingerprintAsync(); - } - } -} diff --git a/src/iOS.Extension/LockFingerprintViewController.designer.cs b/src/iOS.Extension/LockFingerprintViewController.designer.cs deleted file mode 100644 index 6fb08458b..000000000 --- a/src/iOS.Extension/LockFingerprintViewController.designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LockFingerprintViewController")] - partial class LockFingerprintViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIButton FingerprintButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIButton UseButton { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("FingerprintButton_TouchUpInside:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void FingerprintButton_TouchUpInside (UIKit.UIButton sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (FingerprintButton != null) { - FingerprintButton.Dispose (); - FingerprintButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (UseButton != null) { - UseButton.Dispose (); - UseButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LockPasswordViewController.cs b/src/iOS.Extension/LockPasswordViewController.cs deleted file mode 100644 index d51531591..000000000 --- a/src/iOS.Extension/LockPasswordViewController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController - { - public LockPasswordViewController(IntPtr handle) : base(handle) - { } - - public LoadingViewController LoadingController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UIBarButtonItem BaseSubmitButton => SubmitButton; - public override Action Success => () => LoadingController.DismissLockAndContinue(); - - partial void SubmitButton_Activated(UIBarButtonItem sender) - { - CheckPassword(); - } - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - LoadingController.CompleteRequest(null); - } - } -} diff --git a/src/iOS.Extension/LockPasswordViewController.designer.cs b/src/iOS.Extension/LockPasswordViewController.designer.cs deleted file mode 100644 index fdc17a29f..000000000 --- a/src/iOS.Extension/LockPasswordViewController.designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LockPasswordViewController")] - partial class LockPasswordViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UITableView MainTableView { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SubmitButton { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SubmitButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (MainTableView != null) { - MainTableView.Dispose (); - MainTableView = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (SubmitButton != null) { - SubmitButton.Dispose (); - SubmitButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LockPinViewController.cs b/src/iOS.Extension/LockPinViewController.cs deleted file mode 100644 index 2737135be..000000000 --- a/src/iOS.Extension/LockPinViewController.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LockPinViewController : Core.Controllers.LockPinViewController - { - public LockPinViewController(IntPtr handle) : base(handle) - { } - - public LoadingViewController LoadingController { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; - public override UILabel BasePinLabel => PinLabel; - public override UILabel BaseInstructionLabel => InstructionLabel; - public override UITextField BasePinTextField => PinTextField; - public override Action Success => () => LoadingController.DismissLockAndContinue(); - - partial void CancelButton_Activated(UIBarButtonItem sender) - { - LoadingController.CompleteRequest(null); - } - } -} diff --git a/src/iOS.Extension/LockPinViewController.designer.cs b/src/iOS.Extension/LockPinViewController.designer.cs deleted file mode 100644 index 4dfa52722..000000000 --- a/src/iOS.Extension/LockPinViewController.designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LockPinViewController")] - partial class LockPinViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel InstructionLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel PinLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UITextField PinTextField { get; set; } - - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - - if (InstructionLabel != null) { - InstructionLabel.Dispose (); - InstructionLabel = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (PinLabel != null) { - PinLabel.Dispose (); - PinLabel = null; - } - - if (PinTextField != null) { - PinTextField.Dispose (); - PinTextField = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LoginAddViewController.cs b/src/iOS.Extension/LoginAddViewController.cs deleted file mode 100644 index ce1c83613..000000000 --- a/src/iOS.Extension/LoginAddViewController.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Foundation; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LoginAddViewController : Core.Controllers.LoginAddViewController - { - public LoginAddViewController(IntPtr handle) : base(handle) - { } - - public LoginListViewController LoginListController { get; set; } - public LoadingViewController LoadingController { get; set; } - - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelBarButton; - public override UIBarButtonItem BaseSaveButton => SaveBarButton; - - public override Action Success => () => - { - _googleAnalyticsService.TrackExtensionEvent("CreatedLogin"); - if(LoginListController != null) - { - LoginListController.DismissModal(); - } - else if(LoadingController != null) - { - LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, - PasswordCell.TextField.Text, null, null); - } - }; - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - if(LoginListController != null) - { - DismissViewController(true, null); - } - else - { - LoadingController.CompleteRequest(null); - } - } - - async partial void SaveBarButton_Activated(UIBarButtonItem sender) - { - await this.SaveAsync(); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var passwordGeneratorController = navController.TopViewController as PasswordGeneratorViewController; - if(passwordGeneratorController != null) - { - passwordGeneratorController.PasswordOptions = Context.PasswordOptions; - passwordGeneratorController.Parent = this; - } - } - } - } -} diff --git a/src/iOS.Extension/LoginAddViewController.designer.cs b/src/iOS.Extension/LoginAddViewController.designer.cs deleted file mode 100644 index 191c7991b..000000000 --- a/src/iOS.Extension/LoginAddViewController.designer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LoginAddViewController")] - partial class LoginAddViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SaveBarButton { get; set; } - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SaveBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SaveBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (SaveBarButton != null) { - SaveBarButton.Dispose (); - SaveBarButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs deleted file mode 100644 index b332e51cc..000000000 --- a/src/iOS.Extension/LoginListViewController.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Linq; -using Bit.iOS.Extension.Models; -using Foundation; -using UIKit; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Core; -using MobileCoreServices; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.iOS.Core.Views; - -namespace Bit.iOS.Extension -{ - public partial class LoginListViewController : ExtendedUITableViewController - { - public LoginListViewController(IntPtr handle) : base(handle) - { } - - public Context Context { get; set; } - public LoadingViewController LoadingController { get; set; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public async override void ViewDidLoad() - { - base.ViewDidLoad(); - NavItem.Title = AppResources.Items; - if(!CanAutoFill()) - { - CancelBarButton.Title = AppResources.Close; - } - else - { - CancelBarButton.Title = AppResources.Cancel; - } - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; - TableView.Source = new TableSource(this); - await ((TableSource)TableView.Source).LoadItemsAsync(); - } - - public bool CanAutoFill() - { - if(Context.ProviderType != Constants.UTTypeAppExtensionFillBrowserAction - && Context.ProviderType != Constants.UTTypeAppExtensionFillWebViewAction - && Context.ProviderType != UTType.PropertyList) - { - return true; - } - - return Context.Details?.HasPasswordField ?? false; - - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - LoadingController.CompleteRequest(null); - } - - partial void AddBarButton_Activated(UIBarButtonItem sender) - { - PerformSegue("loginAddSegue", this); - } - - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) - { - var navController = segue.DestinationViewController as UINavigationController; - if(navController != null) - { - var addLoginController = navController.TopViewController as LoginAddViewController; - if(addLoginController != null) - { - addLoginController.Context = Context; - addLoginController.LoginListController = this; - } - } - } - - public void DismissModal() - { - DismissViewController(true, async () => - { - await ((TableSource)TableView.Source).LoadItemsAsync(); - TableView.ReloadData(); - }); - } - - public class TableSource : ExtensionTableSource - { - private LoginListViewController _controller; - - public TableSource(LoginListViewController controller) - : base(controller.Context, controller) - { - _controller = controller; - } - - public override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - tableView.DeselectRow(indexPath, true); - tableView.EndEditing(true); - - if(Items == null || Items.Count() == 0) - { - _controller.PerformSegue("loginAddSegue", this); - return; - } - - var item = Items.ElementAt(indexPath.Row); - if(item == null) - { - _controller.LoadingController.CompleteRequest(null); - return; - } - - if(_controller.CanAutoFill() && !string.IsNullOrWhiteSpace(item.Password)) - { - string totp = null; - if(!_settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) - { - totp = GetTotp(item); - } - - _controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password, - item.Fields.Value, totp); - } - else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) || - !string.IsNullOrWhiteSpace(item.Totp.Value)) - { - var sheet = Dialogs.CreateActionSheet(item.Name, _controller); - if(!string.IsNullOrWhiteSpace(item.Username)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyUsername, UIAlertActionStyle.Default, a => - { - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = item.Username; - var alert = Dialogs.CreateMessageAlert(AppResources.CopyUsername); - _controller.PresentViewController(alert, true, () => - { - _controller.DismissViewController(true, null); - }); - })); - } - - if(!string.IsNullOrWhiteSpace(item.Password)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyPassword, UIAlertActionStyle.Default, a => - { - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = item.Password; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedPassword); - _controller.PresentViewController(alert, true, () => - { - _controller.DismissViewController(true, null); - }); - })); - } - - if(!string.IsNullOrWhiteSpace(item.Totp.Value)) - { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a => - { - var totp = GetTotp(item); - if(string.IsNullOrWhiteSpace(totp)) - { - return; - } - - UIPasteboard clipboard = UIPasteboard.General; - clipboard.String = totp; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp); - _controller.PresentViewController(alert, true, () => - { - _controller.DismissViewController(true, null); - }); - })); - } - - sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null)); - _controller.PresentViewController(sheet, true, null); - } - else - { - var alert = Dialogs.CreateAlert(null, AppResources.NoUsernamePasswordConfigured, AppResources.Ok); - _controller.PresentViewController(alert, true, null); - } - } - } - } -} diff --git a/src/iOS.Extension/LoginListViewController.designer.cs b/src/iOS.Extension/LoginListViewController.designer.cs deleted file mode 100644 index aa1abf2ea..000000000 --- a/src/iOS.Extension/LoginListViewController.designer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("LoginListViewController")] - partial class LoginListViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem AddBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Action ("AddBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (AddBarButton != null) { - AddBarButton.Dispose (); - AddBarButton = null; - } - - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/Main.cs b/src/iOS.Extension/Main.cs deleted file mode 100644 index 9291201f8..000000000 --- a/src/iOS.Extension/Main.cs +++ /dev/null @@ -1,15 +0,0 @@ -using UIKit; - -namespace Bit.iOS.Extension -{ - public class Application - { - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, "AppDelegate"); - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/MainInterface.storyboard b/src/iOS.Extension/MainInterface.storyboard deleted file mode 100644 index f107fe8ef..000000000 --- a/src/iOS.Extension/MainInterface.storyboard +++ /dev/null @@ -1,615 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/iOS.Extension/Models/Context.cs b/src/iOS.Extension/Models/Context.cs deleted file mode 100644 index 94015819e..000000000 --- a/src/iOS.Extension/Models/Context.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Foundation; -using Bit.iOS.Core.Models; - -namespace Bit.iOS.Extension.Models -{ - public class Context : AppExtensionContext - { - private string _uriString; - - public NSExtensionContext ExtContext { get; set; } - public string ProviderType { get; set; } - public string LoginTitle { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string OldPassword { get; set; } - public string Notes { get; set; } - public PageDetails Details { get; set; } - } -} diff --git a/src/iOS.Extension/Models/FillScript.cs b/src/iOS.Extension/Models/FillScript.cs deleted file mode 100644 index 0364d14dd..000000000 --- a/src/iOS.Extension/Models/FillScript.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using System.Text.RegularExpressions; - -namespace Bit.iOS.Extension.Models -{ - public class FillScript - { - private static string[] _usernameFieldNames = new[]{ "username", "user name", "email", - "email address", "e-mail", "e-mail address", "userid", "user id" }; - - public FillScript(PageDetails pageDetails, string fillUsername, string fillPassword, - List> fillFields) - { - if(pageDetails == null) - { - return; - } - - DocumentUUID = pageDetails.DocumentUUID; - - var filledFields = new Dictionary(); - - if(fillFields?.Any() ?? false) - { - var fieldNames = fillFields.Select(f => f.Item1?.ToLower()).ToArray(); - foreach(var field in pageDetails.Fields.Where(f => f.Viewable)) - { - if(filledFields.ContainsKey(field.OpId)) - { - continue; - } - - var matchingIndex = FindMatchingFieldIndex(field, fieldNames); - if(matchingIndex > -1) - { - filledFields.Add(field.OpId, field); - Script.Add(new List { "click_on_opid", field.OpId }); - Script.Add(new List { "fill_by_opid", field.OpId, fillFields[matchingIndex].Item2 }); - } - } - } - - if(string.IsNullOrWhiteSpace(fillPassword)) - { - // No password for this login. Maybe they just wanted to auto-fill some custom fields? - SetFillScriptForFocus(filledFields); - return; - } - - List usernames = new List(); - List passwords = new List(); - - var passwordFields = pageDetails.Fields.Where(f => f.Type == "password" && f.Viewable).ToArray(); - if(!passwordFields.Any()) - { - // not able to find any viewable password fields. maybe there are some "hidden" ones? - passwordFields = pageDetails.Fields.Where(f => f.Type == "password").ToArray(); - } - - foreach(var form in pageDetails.Forms) - { - var passwordFieldsForForm = passwordFields.Where(f => f.Form == form.Key).ToArray(); - passwords.AddRange(passwordFieldsForForm); - - if(string.IsNullOrWhiteSpace(fillUsername)) - { - continue; - } - - foreach(var pf in passwordFieldsForForm) - { - var username = FindUsernameField(pageDetails, pf, false, true); - if(username == null) - { - // not able to find any viewable username fields. maybe there are some "hidden" ones? - username = FindUsernameField(pageDetails, pf, true, true); - } - - if(username != null) - { - usernames.Add(username); - } - } - } - - if(passwordFields.Any() && !passwords.Any()) - { - // The page does not have any forms with password fields. Use the first password field on the page and the - // input field just before it as the username. - - var pf = passwordFields.First(); - passwords.Add(pf); - - if(!string.IsNullOrWhiteSpace(fillUsername) && pf.ElementNumber > 0) - { - var username = FindUsernameField(pageDetails, pf, false, false); - if(username == null) - { - // not able to find any viewable username fields. maybe there are some "hidden" ones? - username = FindUsernameField(pageDetails, pf, true, false); - } - - if(username != null) - { - usernames.Add(username); - } - } - } - - if(!passwordFields.Any()) - { - // No password fields on this page. Let's try to just fuzzy fill the username. - var usernameFieldNamesList = _usernameFieldNames.ToList(); - foreach(var f in pageDetails.Fields) - { - if(f.Viewable && (f.Type == "text" || f.Type == "email" || f.Type == "tel") && - FieldIsFuzzyMatch(f, usernameFieldNamesList)) - { - usernames.Add(f); - } - } - } - - foreach(var username in usernames.Where(u => !filledFields.ContainsKey(u.OpId))) - { - filledFields.Add(username.OpId, username); - Script.Add(new List { "click_on_opid", username.OpId }); - Script.Add(new List { "fill_by_opid", username.OpId, fillUsername }); - } - - foreach(var password in passwords.Where(p => !filledFields.ContainsKey(p.OpId))) - { - filledFields.Add(password.OpId, password); - Script.Add(new List { "click_on_opid", password.OpId }); - Script.Add(new List { "fill_by_opid", password.OpId, fillPassword }); - } - - SetFillScriptForFocus(filledFields); - } - - private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden, - bool checkForm) - { - PageDetails.Field usernameField = null; - - foreach(var f in pageDetails.Fields) - { - if(f.ElementNumber >= passwordField.ElementNumber) - { - break; - } - - if((!checkForm || f.Form == passwordField.Form) - && (canBeHidden || f.Viewable) - && f.ElementNumber < passwordField.ElementNumber - && (f.Type == "text" || f.Type == "email" || f.Type == "tel")) - { - usernameField = f; - - if(FindMatchingFieldIndex(f, _usernameFieldNames) > -1) - { - // We found an exact match. No need to keep looking. - break; - } - } - } - - return usernameField; - } - - private int FindMatchingFieldIndex(PageDetails.Field field, string[] names) - { - var matchingIndex = -1; - if(!string.IsNullOrWhiteSpace(field.HtmlId)) - { - matchingIndex = Array.IndexOf(names, field.HtmlId.ToLower()); - } - if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.HtmlName)) - { - matchingIndex = Array.IndexOf(names, field.HtmlName.ToLower()); - } - if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.LabelTag)) - { - matchingIndex = Array.IndexOf(names, CleanLabel(field.LabelTag)); - } - if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.Placeholder)) - { - matchingIndex = Array.IndexOf(names, field.Placeholder.ToLower()); - } - - return matchingIndex; - } - - private bool FieldIsFuzzyMatch(PageDetails.Field field, List names) - { - if(!string.IsNullOrWhiteSpace(field.HtmlId) && FuzzyMatch(names, field.HtmlId.ToLower())) - { - return true; - } - if(!string.IsNullOrWhiteSpace(field.HtmlName) && FuzzyMatch(names, field.HtmlName.ToLower())) - { - return true; - } - if(!string.IsNullOrWhiteSpace(field.LabelTag) && FuzzyMatch(names, CleanLabel(field.LabelTag))) - { - return true; - } - if(!string.IsNullOrWhiteSpace(field.Placeholder) && FuzzyMatch(names, field.Placeholder.ToLower())) - { - return true; - } - - return false; - } - - private bool FuzzyMatch(List options, string value) - { - if((!options?.Any() ?? true) || string.IsNullOrWhiteSpace(value)) - { - return false; - } - - return options.Any(o => value.Contains(o)); - } - - private void SetFillScriptForFocus(IDictionary filledFields) - { - if(!filledFields.Any()) - { - return; - } - - PageDetails.Field lastField = null, lastPasswordField = null; - foreach(var field in filledFields) - { - if(field.Value.Viewable) - { - lastField = field.Value; - if(field.Value.Type == "password") - { - lastPasswordField = field.Value; - } - } - } - - // Prioritize password field over others. - if(lastPasswordField != null) - { - Script.Add(new List { "focus_by_opid", lastPasswordField.OpId }); - } - else if(lastField != null) - { - Script.Add(new List { "focus_by_opid", lastField.OpId }); - } - } - - private string CleanLabel(string label) - { - return Regex.Replace(label, @"(?:\r\n|\r|\n)", string.Empty).Trim().ToLower(); - } - - [JsonProperty(PropertyName = "script")] - public List> Script { get; set; } = new List>(); - [JsonProperty(PropertyName = "documentUUID")] - public object DocumentUUID { get; set; } - [JsonProperty(PropertyName = "properties")] - public object Properties { get; set; } = new object(); - [JsonProperty(PropertyName = "options")] - public object Options { get; set; } = new { animate = false }; - [JsonProperty(PropertyName = "metadata")] - public object MetaData { get; set; } = new object(); - } -} diff --git a/src/iOS.Extension/Models/PageDetails.cs b/src/iOS.Extension/Models/PageDetails.cs deleted file mode 100644 index 004173bfc..000000000 --- a/src/iOS.Extension/Models/PageDetails.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Bit.iOS.Extension.Models -{ - public class PageDetails - { - public string DocumentUUID { get; set; } - public string Title { get; set; } - public string Url { get; set; } - public string DocumentUrl { get; set; } - public string TabUrl { get; set; } - public Dictionary Forms { get; set; } - public List Fields { get; set; } - public long CollectedTimestamp { get; set; } - public bool HasPasswordField => Fields.Any(f => f.Type == "password"); - - public class Form - { - public string OpId { get; set; } - public string HtmlName { get; set; } - public string HtmlId { get; set; } - public string HtmlAction { get; set; } - public string HtmlMethod { get; set; } - } - - public class Field - { - public string OpId { get; set; } - public int ElementNumber { get; set; } - public bool Visible { get; set; } - public bool Viewable { get; set; } - public string HtmlId { get; set; } - public string HtmlName { get; set; } - public string HtmlClass { get; set; } - public string LabelRight { get; set; } - public string LabelLeft { get; set; } - [JsonProperty("label-tag")] - public string LabelTag { get; set; } - public string Placeholder { get; set; } - public string Type { get; set; } - public string Value { get; set; } - public bool Disabled { get; set; } - public bool Readonly { get; set; } - public string OnePasswordFieldType { get; set; } - public string Form { get; set; } - } - } - -} diff --git a/src/iOS.Extension/PasswordGeneratorViewController.cs b/src/iOS.Extension/PasswordGeneratorViewController.cs deleted file mode 100644 index 4b015c243..000000000 --- a/src/iOS.Extension/PasswordGeneratorViewController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class PasswordGeneratorViewController : Core.Controllers.PasswordGeneratorViewController - { - public PasswordGeneratorViewController(IntPtr handle) : base(handle, false) - { } - - public LoginAddViewController Parent { get; set; } - public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelBarButton; - public override UIBarButtonItem BaseSelectBarButton => SelectBarButton; - public override UILabel BasePasswordLabel => PasswordLabel; - - partial void SelectBarButton_Activated(UIBarButtonItem sender) - { - GoogleAnalyticsService.TrackExtensionEvent("SelectedGeneratedPassword"); - DismissViewController(true, () => - { - Parent.PasswordCell.TextField.Text = PasswordLabel.Text; - }); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) - { - DismissViewController(true, null); - } - } -} diff --git a/src/iOS.Extension/PasswordGeneratorViewController.designer.cs b/src/iOS.Extension/PasswordGeneratorViewController.designer.cs deleted file mode 100644 index 950ebf7a5..000000000 --- a/src/iOS.Extension/PasswordGeneratorViewController.designer.cs +++ /dev/null @@ -1,82 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("PasswordGeneratorViewController")] - partial class PasswordGeneratorViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIView BaseView { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIView OptionsContainer { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel PasswordLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SelectBarButton { get; set; } - - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("SelectBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SelectBarButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (BaseView != null) { - BaseView.Dispose (); - BaseView = null; - } - - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - - if (OptionsContainer != null) { - OptionsContainer.Dispose (); - OptionsContainer = null; - } - - if (PasswordLabel != null) { - PasswordLabel.Dispose (); - PasswordLabel = null; - } - - if (SelectBarButton != null) { - SelectBarButton.Dispose (); - SelectBarButton = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/Properties/AssemblyInfo.cs b/src/iOS.Extension/Properties/AssemblyInfo.cs deleted file mode 100644 index 183860705..000000000 --- a/src/iOS.Extension/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("BitwardeniOSExtension")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("8bit Solutions LLC")] -[assembly: AssemblyProduct("Bitwarden")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("32f5a2d6-f54d-4da1-ae26-0a980d48f422")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/iOS.Extension/Resources/Icon.png b/src/iOS.Extension/Resources/Icon.png deleted file mode 100644 index e4b594ad5..000000000 Binary files a/src/iOS.Extension/Resources/Icon.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/Icon@2x.png b/src/iOS.Extension/Resources/Icon@2x.png deleted file mode 100644 index 37369f4d3..000000000 Binary files a/src/iOS.Extension/Resources/Icon@2x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/Icon@3x.png b/src/iOS.Extension/Resources/Icon@3x.png deleted file mode 100644 index 0bfafae46..000000000 Binary files a/src/iOS.Extension/Resources/Icon@3x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/ext-icon.png b/src/iOS.Extension/Resources/ext-icon.png deleted file mode 100644 index 2663f2a8e..000000000 Binary files a/src/iOS.Extension/Resources/ext-icon.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/ext-icon@2x.png b/src/iOS.Extension/Resources/ext-icon@2x.png deleted file mode 100644 index c2ea619de..000000000 Binary files a/src/iOS.Extension/Resources/ext-icon@2x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/ext-icon@3x.png b/src/iOS.Extension/Resources/ext-icon@3x.png deleted file mode 100644 index 96e4d1ce0..000000000 Binary files a/src/iOS.Extension/Resources/ext-icon@3x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/fingerprint.png b/src/iOS.Extension/Resources/fingerprint.png deleted file mode 100644 index 754e39825..000000000 Binary files a/src/iOS.Extension/Resources/fingerprint.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/fingerprint@2x.png b/src/iOS.Extension/Resources/fingerprint@2x.png deleted file mode 100644 index 79547a076..000000000 Binary files a/src/iOS.Extension/Resources/fingerprint@2x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/fingerprint@3x.png b/src/iOS.Extension/Resources/fingerprint@3x.png deleted file mode 100644 index 236c8da73..000000000 Binary files a/src/iOS.Extension/Resources/fingerprint@3x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/logo.png b/src/iOS.Extension/Resources/logo.png deleted file mode 100644 index 33ced5b12..000000000 Binary files a/src/iOS.Extension/Resources/logo.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/logo@2x.png b/src/iOS.Extension/Resources/logo@2x.png deleted file mode 100644 index 2a0ba60b9..000000000 Binary files a/src/iOS.Extension/Resources/logo@2x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/logo@3x.png b/src/iOS.Extension/Resources/logo@3x.png deleted file mode 100644 index 904731676..000000000 Binary files a/src/iOS.Extension/Resources/logo@3x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/smile.png b/src/iOS.Extension/Resources/smile.png deleted file mode 100644 index 25f2f45b1..000000000 Binary files a/src/iOS.Extension/Resources/smile.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/smile@2x.png b/src/iOS.Extension/Resources/smile@2x.png deleted file mode 100644 index 71dd21e35..000000000 Binary files a/src/iOS.Extension/Resources/smile@2x.png and /dev/null differ diff --git a/src/iOS.Extension/Resources/smile@3x.png b/src/iOS.Extension/Resources/smile@3x.png deleted file mode 100644 index 6e7189e69..000000000 Binary files a/src/iOS.Extension/Resources/smile@3x.png and /dev/null differ diff --git a/src/iOS.Extension/SetupViewController.cs b/src/iOS.Extension/SetupViewController.cs deleted file mode 100644 index abeaaafc4..000000000 --- a/src/iOS.Extension/SetupViewController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Bit.iOS.Extension.Models; -using UIKit; -using Plugin.Settings.Abstractions; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; - -namespace Bit.iOS.Extension -{ - public partial class SetupViewController : ExtendedUIViewController - { - public SetupViewController(IntPtr handle) : base(handle) - { } - - public Context Context { get; set; } - public LoadingViewController LoadingController { get; set; } - - public override void ViewWillAppear(bool animated) - { - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - base.ViewWillAppear(animated); - } - - public override void ViewDidLoad() - { - View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); - var descriptor = UIFontDescriptor.PreferredBody; - DescriptionLabel.Text = $@"{AppResources.ExtensionSetup} - -{AppResources.ExtensionSetup2}"; - DescriptionLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize); - DescriptionLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - ActivatedLabel.Text = AppResources.ExtensionActivated; - ActivatedLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize * 1.3f); - - BackButton.Title = AppResources.Back; - base.ViewDidLoad(); - } - - partial void BackButton_Activated(UIBarButtonItem sender) - { - LoadingController.CompleteRequest(null); - } - } -} diff --git a/src/iOS.Extension/SetupViewController.designer.cs b/src/iOS.Extension/SetupViewController.designer.cs deleted file mode 100644 index 00c83f807..000000000 --- a/src/iOS.Extension/SetupViewController.designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -// WARNING -// -// This file has been generated automatically by Xamarin Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. -// -using Foundation; -using System; -using System.CodeDom.Compiler; -using UIKit; - -namespace Bit.iOS.Extension -{ - [Register ("SetupViewController")] - partial class SetupViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel ActivatedLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem BackButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UILabel DescriptionLabel { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIImageView IconImage { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } - - [Action ("BackButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void BackButton_Activated (UIKit.UIBarButtonItem sender); - - void ReleaseDesignerOutlets () - { - if (ActivatedLabel != null) { - ActivatedLabel.Dispose (); - ActivatedLabel = null; - } - - if (BackButton != null) { - BackButton.Dispose (); - BackButton = null; - } - - if (DescriptionLabel != null) { - DescriptionLabel.Dispose (); - DescriptionLabel = null; - } - - if (IconImage != null) { - IconImage.Dispose (); - IconImage = null; - } - - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - } - } -} \ No newline at end of file diff --git a/src/iOS.Extension/extension.js b/src/iOS.Extension/extension.js deleted file mode 100644 index 6bbad37f2..000000000 --- a/src/iOS.Extension/extension.js +++ /dev/null @@ -1,101 +0,0 @@ -var BitwardenExtension = function () { }; - -BitwardenExtension.prototype = { - run: function (arguments) { - console.log('Run'); - console.log(arguments); - - var args = { - 'url_string': document.URL, - pageDetails: this.collect(document) - }; - - console.log(args); - arguments.completionFunction(args); - }, - finalize: function (arguments) { - console.log('Finalize'); - console.log(arguments); - - if (arguments.fillScript) { - this.fill(document, JSON.parse(arguments.fillScript)); - } - }, - - /* - 1Password Extension - - Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov. - Copyright (c) 2014 AgileBits. All rights reserved. - - ================================================================================ - - Copyright (c) 2014 AgileBits Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - - collect: function(document, undefined) { - var isFirefox = false, isChrome = false, isSafari = true; - document.elementsByOPID={};document.addEventListener('input',function(b){!1!==b.a&&'input'===b.target.tagName.toLowerCase()&&(b.target.dataset['com.agilebits.onepassword.userEdited']='yes')},!0); - function q(b,d){function f(a,e){var c=a[e];if('string'==typeof c)return c;c=a.getAttribute(e);return'string'==typeof c?c:null}function h(a,e){if(-1===['text','password'].indexOf(e.type.toLowerCase())||!(m.test(a.value)||m.test(a.htmlID)||m.test(a.htmlName)||m.test(a.placeholder)||m.test(a['label-tag'])||m.test(a['label-data'])||m.test(a['label-aria'])))return!1;if(!a.visible)return!0;if('password'==e.type.toLowerCase())return!1;var c=e.type;v(e,!0);return c!==e.type}function n(a){switch(p(a.type)){case 'checkbox':return a.checked? - '✓':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254\\?]/mg,''):null;return[c?c:null,a.value]}),{options:a}):null}function r(a){var e;for(a=a.parentElement||a.parentNode;a&&'td'!=p(a.tagName);)a=a.parentElement||a.parentNode;if(!a|| - void 0===a)return null;e=a.parentElement||a.parentNode;if('tr'!=e.tagName.toLowerCase())return null;e=e.previousElementSibling;if(!e||'tr'!=(e.tagName+'').toLowerCase()||e.cells&&a.cellIndex>=e.cells.length)return null;a=e.cells[a.cellIndex];a=a.textContent||a.innerText;return a=x(a)}function s(a){var e,c=[];if(a.labels&&a.labels.length&&0b.clientWidth||10>b.clientHeight)return!1;var s=b.getClientRects();if(0===s.length)return!1;for(var g=0;gh||0>r.right)return!1;if(0>l||l>h||0>d||d>n)return!1;for(f=b.ownerDocument.elementFromPoint(l+(f.right>window.innerWidth?(window.innerWidth-l)/2:f.width/2),d+(f.bottom>window.innerHeight? - (window.innerHeight-d)/2:f.height/2));f&&f!==b&&f!==document;){if(f.tagName&&'string'===typeof f.tagName&&'label'===f.tagName.toLowerCase()&&b.labels&&0 - - - Debug - iPhoneSimulator - {32F5A2D6-F54D-4DA1-AE26-0A980D48F422} - {EE2C853D-36AF-4FDB-B1AD-8E90477E2198};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Exe - Bit.iOS.Extension - BitwardeniOSExtension - Resources - Properties - - - - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG - prompt - 4 - false - i386, x86_64 - None - True - Entitlements.plist - 9.3 - False - False - False - False - True - False - False - Default - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler - - - none - true - bin\iPhoneSimulator\Release - prompt - 4 - Full - i386, x86_64 - false - Entitlements.plist - 9.3 - False - False - False - False - False - False - True - Default - NSUrlSessionHandler - False - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - - - true - full - false - bin\iPhone\Debug - DEBUG - prompt - 4 - false - ARM64 - Entitlements.plist - iPhone Developer - True - - - - - None - False - False - False - False - False - True - False - False - --http-message-handler=NSUrlSessionHandler - 10.2 - Default - NSUrlSessionHandler - - - none - true - bin\iPhone\Release - prompt - 4 - Entitlements.plist - ARM64 - false - iPhone Developer - Full - False - False - False - False - False - False - True - False - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - - - none - True - bin\iPhone\Ad-Hoc - prompt - 4 - False - ARM64 - Entitlements.plist - True - Automatic:AdHoc - iPhone Distribution - Full - False - False - False - False - False - False - True - False - NSUrlSessionHandler - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - - - none - True - bin\iPhone\AppStore - prompt - 4 - False - ARM64 - Entitlements.plist - Automatic:AppStore - iPhone Distribution - Full - False - False - False - False - False - False - True - False - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - 10.2 - Default - NSUrlSessionHandler - - - 9.3 - Full - False - False - False - i386, x86_64 - False - False - False - True - Default - NSUrlSessionHandler - False - bin\iPhoneSimulator\Ad-Hoc - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - Entitlements.plist - - - 9.3 - Full - False - False - False - i386, x86_64 - False - False - False - True - Default - NSUrlSessionHandler - False - bin\iPhoneSimulator\AppStore - --http-message-handler=NSUrlSessionHandler --linkskip=BitwardeniOS --linkskip=BitwardeniOSCore --linkskip=BitwardeniOSExtension --linkskip=BitwardenApp --linkskip=SQLite-net - - - - - LockPasswordViewController.cs - - - - SetupViewController.cs - - - - LockPinViewController.cs - - - - LockFingerprintViewController.cs - - - - PasswordGeneratorViewController.cs - - - - LoginAddViewController.cs - - - - LoginListViewController.cs - - - - - - - - - LoadingViewController.cs - - - - - - - - - - - - - - - - Always - - - - - - - - - - - - - - - - - - - - {8a279ee4-4537-4656-9c93-44945e594556} - App - - - {B2538ADA-B605-4D6F-ACD2-62A409680F84} - iOS.Core - False - False - - - - - 4.4.0 - - - 2.0.5782 - - - - \ No newline at end of file diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 23f02c24c..295ad42e6 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -1,409 +1,35 @@ using System; -using XLabs.Ioc; +using System.Collections.Generic; +using System.Linq; + using Foundation; using UIKit; -using Bit.App.Abstractions; -using Bit.App.Services; -using Bit.iOS.Services; -using Plugin.Connectivity; -using Bit.App.Repositories; -using Plugin.Fingerprint; -using Plugin.Settings.Abstractions; -using System.Diagnostics; -using Xamarin.Forms; -using Bit.iOS.Core.Services; -using Plugin.Connectivity.Abstractions; -using Bit.App.Pages; -using HockeyApp.iOS; -using Bit.iOS.Core; -using SimpleInjector; -using XLabs.Ioc.SimpleInjectorContainer; -using CoreNFC; -using Bit.App.Resources; -using AuthenticationServices; -using System.Threading.Tasks; -using Bit.App.Models; -using System.Linq; -using System.Collections.Generic; -using Bit.iOS.Core.Utilities; namespace Bit.iOS { + // The UIApplicationDelegate for the application. This class is responsible for launching the + // User Interface of the application, as well as listening (and optionally responding) to + // application events from iOS. [Register("AppDelegate")] - public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate + public partial class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate { - private NFCNdefReaderSession _nfcSession = null; - private ILockService _lockService; - private IDeviceInfoService _deviceInfoService; - private ICipherService _cipherService; - private iOSPushNotificationHandler _pushHandler = null; - private NFCReaderDelegate _nfcDelegate = null; - - public ISettings Settings { get; set; } - + // + // This method is invoked when the application has loaded and is ready to run. In this + // method you should instantiate the window, load the UI into it and then make the window + // visible. + // + // You have 17 seconds to return from this method, or iOS will terminate your application. + // public override bool FinishedLaunching(UIApplication app, NSDictionary options) { - Forms.Init(); + Xamarin.Forms.Forms.Init(); - if(!Resolver.IsSet) - { - SetIoc(); - } + FFImageLoading.Forms.Platform.CachedImageRenderer.Init(); - _lockService = Resolver.Resolve(); - _deviceInfoService = Resolver.Resolve(); - _cipherService = Resolver.Resolve(); - _pushHandler = new iOSPushNotificationHandler(Resolver.Resolve()); - _nfcDelegate = new NFCReaderDelegate((success, message) => ProcessYubikey(success, message)); - var appIdService = Resolver.Resolve(); - - var crashManagerDelegate = new HockeyAppCrashManagerDelegate( - appIdService, Resolver.Resolve()); - var manager = BITHockeyManager.SharedHockeyManager; - manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); - manager.CrashManager.CrashManagerStatus = BITCrashManagerStatus.AutoSend; - manager.UserId = appIdService.AppId; - manager.StartManager(); - manager.Authenticator.AuthenticateInstallation(); - manager.DisableMetricsManager = manager.DisableFeedbackManager = manager.DisableUpdateManager = true; - - LoadApplication(new App.App( - null, - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - _lockService, - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve(), - Resolver.Resolve())); - - // Appearance stuff - - var primaryColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f); - var grayLight = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - UIBarButtonItem.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).TintColor = primaryColor; - UIButton.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).SetTitleColor(primaryColor, - UIControlState.Normal); - UIButton.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).TintColor = primaryColor; - UIStepper.Appearance.TintColor = grayLight; - UISlider.Appearance.TintColor = primaryColor; - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "ShowAppExtension", (sender, page) => - { - var itemProvider = new NSItemProvider(new NSDictionary(), Core.Constants.UTTypeAppExtensionSetup); - var extensionItem = new NSExtensionItem(); - extensionItem.Attachments = new NSItemProvider[] { itemProvider }; - var activityViewController = new UIActivityViewController(new NSExtensionItem[] { extensionItem }, null); - activityViewController.CompletionHandler = (activityType, completed) => - { - page.EnabledExtension(completed && activityType == "com.8bit.bitwarden.find-login-action-extension"); - }; - - var modal = UIApplication.SharedApplication.KeyWindow.RootViewController.ModalViewController; - if(activityViewController.PopoverPresentationController != null) - { - activityViewController.PopoverPresentationController.SourceView = modal.View; - var frame = UIScreen.MainScreen.Bounds; - frame.Height /= 2; - activityViewController.PopoverPresentationController.SourceRect = frame; - } - - modal.PresentViewController(activityViewController, true, null); - }); - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender, listen) => - { - if(_deviceInfoService.NfcEnabled) - { - _nfcSession?.InvalidateSession(); - _nfcSession?.Dispose(); - _nfcSession = null; - if(listen) - { - _nfcSession = new NFCNdefReaderSession(_nfcDelegate, null, true); - _nfcSession.AlertMessage = AppResources.HoldYubikeyNearTop; - _nfcSession.BeginSession(); - } - } - }); - - UIApplication.SharedApplication.StatusBarHidden = false; - UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "ShowStatusBar", (sender, show) => - { - UIApplication.SharedApplication.SetStatusBarHidden(!show, false); - }); - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "FullSyncCompleted", async (sender, successfully) => - { - if(_deviceInfoService.Version >= 12 && successfully) - { - await ASHelpers.ReplaceAllIdentities(_cipherService); - } - }); - - MessagingCenter.Subscribe>( - Xamarin.Forms.Application.Current, "UpsertedCipher", async (sender, data) => - { - if(_deviceInfoService.Version >= 12) - { - if(await ASHelpers.IdentitiesCanIncremental()) - { - if(data.Item2) - { - var identity = await ASHelpers.GetCipherIdentityAsync(data.Item1, _cipherService); - if(identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - } - await ASHelpers.ReplaceAllIdentities(_cipherService); - } - }); - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "DeletedCipher", async (sender, cipher) => - { - if(_deviceInfoService.Version >= 12) - { - if(await ASHelpers.IdentitiesCanIncremental()) - { - var identity = ASHelpers.ToCredentialIdentity(cipher); - if(identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - await ASHelpers.ReplaceAllIdentities(_cipherService); - } - }); - - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "LoggedOut", async (sender) => - { - if(_deviceInfoService.Version >= 12) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - }); + LoadApplication(new App.App(null)); ZXing.Net.Mobile.Forms.iOS.Platform.Init(); return base.FinishedLaunching(app, options); } - - public override void DidEnterBackground(UIApplication uiApplication) - { - var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) - { - Tag = 4321 - }; - - var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) - { - BackgroundColor = new UIColor(red: 0.93f, green: 0.94f, blue: 0.96f, alpha: 1.0f) - }; - - var imageView = new UIImageView(new UIImage("logo.png")) - { - Center = new CoreGraphics.CGPoint(view.Center.X, view.Center.Y - 30) - }; - - view.AddSubview(backgroundView); - view.AddSubview(imageView); - - UIApplication.SharedApplication.KeyWindow.AddSubview(view); - UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); - UIApplication.SharedApplication.KeyWindow.EndEditing(true); - UIApplication.SharedApplication.SetStatusBarHidden(true, false); - - // Log the date/time we last backgrounded - _lockService.UpdateLastActivity(); - - // Dispatch Google Analytics - SendGoogleAnalyticsHitsInBackground(); - - base.DidEnterBackground(uiApplication); - Debug.WriteLine("DidEnterBackground"); - } - - public override void OnResignActivation(UIApplication uiApplication) - { - base.OnResignActivation(uiApplication); - Debug.WriteLine("OnResignActivation"); - } - - public override void WillTerminate(UIApplication uiApplication) - { - base.WillTerminate(uiApplication); - Debug.WriteLine("WillTerminate"); - } - - public override void OnActivated(UIApplication uiApplication) - { - base.OnActivated(uiApplication); - Debug.WriteLine("OnActivated"); - - UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; - - var view = UIApplication.SharedApplication.KeyWindow.ViewWithTag(4321); - if(view != null) - { - view.RemoveFromSuperview(); - UIApplication.SharedApplication.SetStatusBarHidden(false, false); - } - } - - public override void WillEnterForeground(UIApplication uiApplication) - { - SendResumedMessage(); - - base.WillEnterForeground(uiApplication); - Debug.WriteLine("WillEnterForeground"); - } - - public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, - NSObject annotation) - { - return true; - } - - public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error) - { - _pushHandler?.OnErrorReceived(error); - } - - public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) - { - _pushHandler?.OnRegisteredSuccess(deviceToken); - } - - public override void DidRegisterUserNotificationSettings(UIApplication application, - UIUserNotificationSettings notificationSettings) - { - application.RegisterForRemoteNotifications(); - } - - public override void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, - Action completionHandler) - { - _pushHandler?.OnMessageReceived(userInfo); - } - - public override void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo) - { - _pushHandler?.OnMessageReceived(userInfo); - } - - private void SendResumedMessage() - { - MessagingCenter.Send(Xamarin.Forms.Application.Current, "Resumed", false); - } - - private void SetIoc() - { - var container = new Container(); - - // Services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterInstance(new DeviceInfoService()); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Repositories - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // Other - container.RegisterInstance(CrossConnectivity.Current); - container.RegisterInstance(CrossFingerprint.Current); - - Settings = new Settings("group.com.8bit.bitwarden"); - container.RegisterInstance(Settings); - - // Push - container.RegisterSingleton(); - container.RegisterSingleton(); - - FFImageLoading.Forms.Platform.CachedImageRenderer.Init(); - Resolver.SetResolver(new SimpleInjectorResolver(container)); - } - - /// - /// This method sends any queued hits when the app enters the background. - /// ref: https://developers.google.com/analytics/devguides/collection/ios/v3/dispatch - /// - private void SendGoogleAnalyticsHitsInBackground() - { - var taskExpired = false; - var taskId = UIApplication.SharedApplication.BeginBackgroundTask(() => - { - taskExpired = true; - }); - - if(taskId == UIApplication.BackgroundTaskInvalid) - { - return; - } - } - - private void ProcessYubikey(bool success, string message) - { - if(success) - { - Device.BeginInvokeOnMainThread(() => - { - MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", message); - }); - } - } } } diff --git a/src/iOS/Resources/Assets.xcassets/AppIcons.appiconset/Contents.json b/src/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 72% rename from src/iOS/Resources/Assets.xcassets/AppIcons.appiconset/Contents.json rename to src/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json index 5b671eb96..98f4d035c 100644 --- a/src/iOS/Resources/Assets.xcassets/AppIcons.appiconset/Contents.json +++ b/src/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -4,109 +4,109 @@ "scale": "2x", "size": "20x20", "idiom": "iphone", - "filename": "Icon-40.png" + "filename": "Icon40.png" }, { "scale": "3x", "size": "20x20", "idiom": "iphone", - "filename": "Icon-60.png" + "filename": "Icon60.png" }, { "scale": "2x", "size": "29x29", "idiom": "iphone", - "filename": "Icon-58.png" + "filename": "Icon58.png" }, { "scale": "3x", "size": "29x29", "idiom": "iphone", - "filename": "Icon-87.png" + "filename": "Icon87.png" }, { "scale": "2x", "size": "40x40", "idiom": "iphone", - "filename": "Icon-80.png" + "filename": "Icon80.png" }, { "scale": "3x", "size": "40x40", "idiom": "iphone", - "filename": "Icon-120.png" + "filename": "Icon120.png" }, { "scale": "2x", "size": "60x60", "idiom": "iphone", - "filename": "Icon-120.png" + "filename": "Icon120.png" }, { "scale": "3x", "size": "60x60", "idiom": "iphone", - "filename": "Icon-180.png" + "filename": "Icon180.png" }, { "scale": "1x", "size": "20x20", "idiom": "ipad", - "filename": "Icon-20.png" + "filename": "Icon20.png" }, { "scale": "2x", "size": "20x20", "idiom": "ipad", - "filename": "Icon-40.png" + "filename": "Icon40.png" }, { "scale": "1x", "size": "29x29", "idiom": "ipad", - "filename": "Icon-29.png" + "filename": "Icon29.png" }, { "scale": "2x", "size": "29x29", "idiom": "ipad", - "filename": "Icon-58.png" + "filename": "Icon58.png" }, { "scale": "1x", "size": "40x40", "idiom": "ipad", - "filename": "Icon-40.png" + "filename": "Icon40.png" }, { "scale": "2x", "size": "40x40", "idiom": "ipad", - "filename": "Icon-80.png" + "filename": "Icon80.png" }, { "scale": "1x", "size": "76x76", "idiom": "ipad", - "filename": "Icon-76.png" + "filename": "Icon76.png" }, { "scale": "2x", "size": "76x76", "idiom": "ipad", - "filename": "Icon-152.png" + "filename": "Icon152.png" }, { "scale": "2x", "size": "83.5x83.5", "idiom": "ipad", - "filename": "Icon-167.png" + "filename": "Icon167.png" }, { "scale": "1x", "size": "1024x1024", "idiom": "ios-marketing", - "filename": "Icon-1024.png" + "filename": "Icon1024.png" } ], "properties": {}, diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png new file mode 100644 index 000000000..9174c989a Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png new file mode 100644 index 000000000..9c60a1761 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png new file mode 100644 index 000000000..448d6efb5 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png new file mode 100644 index 000000000..8524768f8 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png new file mode 100644 index 000000000..60a64703c Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png new file mode 100644 index 000000000..45268a641 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png new file mode 100644 index 000000000..6a6c77a8b Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png new file mode 100644 index 000000000..cc7edcf5c Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png new file mode 100644 index 000000000..1ad04f004 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png new file mode 100644 index 000000000..2dd52620a Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png new file mode 100644 index 000000000..b058cae2f Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png new file mode 100644 index 000000000..02e47a261 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png differ diff --git a/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png new file mode 100644 index 000000000..4954a4bd3 Binary files /dev/null and b/src/iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png differ diff --git a/src/iOS/Controls/ContentPageRenderer.cs b/src/iOS/Controls/ContentPageRenderer.cs deleted file mode 100644 index afff68785..000000000 --- a/src/iOS/Controls/ContentPageRenderer.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Bit.iOS.Controls; -using UIKit; -using Xamarin.Forms; -using Xamarin.Forms.Platform.iOS; - -[assembly: ExportRenderer(typeof(ContentPage), typeof(ContentPageRenderer))] -namespace Bit.iOS.Controls -{ - public class ContentPageRenderer : PageRenderer - { - public override void ViewWillAppear(bool animated) - { - base.ViewWillAppear(animated); - - var contentPage = Element as ContentPage; - if(contentPage == null || NavigationController == null) - { - return; - } - - var itemsInfo = contentPage.ToolbarItems; - - var navigationItem = NavigationController.TopViewController.NavigationItem; - var leftNativeButtons = (navigationItem.LeftBarButtonItems ?? new UIBarButtonItem[] { }).ToList(); - var rightNativeButtons = (navigationItem.RightBarButtonItems ?? new UIBarButtonItem[] { }).ToList(); - - var newLeftButtons = new List(); - var newRightButtons = new List(); - - rightNativeButtons.ForEach(nativeItem => - { - // Use reflection to get Xamarin private field "_item" - var field = nativeItem.GetType().GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance); - if(field == null) - { - return; - } - - var info = field.GetValue(nativeItem) as ToolbarItem; - if(info == null) - { - return; - } - - if(info.Priority < 0) - { - newLeftButtons.Add(nativeItem); - } - else - { - newRightButtons.Add(nativeItem); - } - }); - - leftNativeButtons.ForEach(nativeItem => - { - newLeftButtons.Add(nativeItem); - }); - - navigationItem.RightBarButtonItems = newRightButtons.ToArray(); - navigationItem.LeftBarButtonItems = newLeftButtons.ToArray(); - } - } -} diff --git a/src/iOS/Controls/CustomButtonRenderer.cs b/src/iOS/Controls/CustomButtonRenderer.cs deleted file mode 100644 index d54a44465..000000000 --- a/src/iOS/Controls/CustomButtonRenderer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Bit.iOS.Controls; -using UIKit; -using Xamarin.Forms; -using Xamarin.Forms.Platform.iOS; -using System.ComponentModel; - -[assembly: ExportRenderer(typeof(Button), typeof(CustomButtonRenderer))] -namespace Bit.iOS.Controls -{ - public class CustomButtonRenderer : ButtonRenderer - { - protected override void OnElementChanged(ElementChangedEventArgs