mirror of
https://github.com/bitwarden/mobile
synced 2025-12-26 05:03:39 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02ad4adff5 | ||
|
|
faa6904ce3 | ||
|
|
c27da8e7c4 | ||
|
|
3ef5ca9cc0 | ||
|
|
2fbd3b4538 | ||
|
|
b3b21ea6b1 | ||
|
|
a3b4ede8f3 | ||
|
|
10ea6a86e3 | ||
|
|
3b2b37b3b0 | ||
|
|
75e27ffbe3 | ||
|
|
a2cff6da28 | ||
|
|
fe80fd0ba1 | ||
|
|
5c6b9fa471 | ||
|
|
d926565358 | ||
|
|
ce0b8bc62d | ||
|
|
04aeddc5de | ||
|
|
13ffbe911a | ||
|
|
ab04759b0e | ||
|
|
798cfef391 | ||
|
|
3b5cdfe03c | ||
|
|
63449a3832 |
14
.github/scripts/android/clean-fdroid.ps1
vendored
14
.github/scripts/android/clean-fdroid.ps1
vendored
@@ -30,16 +30,6 @@ $xml.Load($androidManifest);
|
||||
$nsAndroid=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$nsAndroid.AddNamespace("android", "http://schemas.android.com/apk/res/android");
|
||||
|
||||
$firebaseReceiver1=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdInternalReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver1.ParentNode.RemoveChild($firebaseReceiver1);
|
||||
|
||||
$firebaseReceiver2=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver2.ParentNode.RemoveChild($firebaseReceiver2);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
Write-Output "########################################"
|
||||
@@ -56,6 +46,10 @@ $firebaseNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns);
|
||||
$firebaseNode.ParentNode.RemoveChild($firebaseNode);
|
||||
|
||||
$daggerNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Google.Dagger']", $ns);
|
||||
$daggerNode.ParentNode.RemoveChild($daggerNode);
|
||||
|
||||
$safetyNetNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.GooglePlayServices.SafetyNet']", $ns);
|
||||
$safetyNetNode.ParentNode.RemoveChild($safetyNetNode);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://ci.appveyor.com/project/bitwarden/mobile)
|
||||
[](https://github.com/bitwarden/mobile/actions/workflows/build.yml?query=branch:master)
|
||||
[](https://crowdin.com/project/bitwarden-mobile)
|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
|
||||
146
appveyor.yml
146
appveyor.yml
@@ -1,146 +0,0 @@
|
||||
image:
|
||||
- Visual Studio 2019
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
except:
|
||||
- l10n_master
|
||||
- gh-pages
|
||||
|
||||
configuration: Release
|
||||
|
||||
stack: node 10
|
||||
|
||||
init:
|
||||
- sh: |
|
||||
if [ "${DEBUG_SSH}" == "true" ]
|
||||
then
|
||||
curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
|
||||
fi
|
||||
- ps: |
|
||||
if($isWindows -and $env:DEBUG_RDP -eq "true") {
|
||||
iex ((new-object net.webclient).DownloadString(`
|
||||
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
}
|
||||
- ps: |
|
||||
if($env:APPVEYOR_REPO_TAG -eq "true") {
|
||||
$tagName = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
|
||||
$env:RELEASE_NAME = "Version ${tagName}"
|
||||
}
|
||||
|
||||
install:
|
||||
- sh: |
|
||||
curl -sflL 'https://raw.githubusercontent.com/appveyor/secure-file/master/install.sh' | bash -e -
|
||||
./appveyor-tools/secure-file -decrypt ./store/fdroid/keystore.jks.enc -secret $FDROID_KEYSTORE_ENC_PASSWORD
|
||||
- sh: npm install
|
||||
- sh: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get -qqy install --no-install-recommends fdroidserver wget
|
||||
- sh: |
|
||||
if [ "${APPVEYOR_REPO_TAG}" == "true" -a "${GH_TOKEN}" != "" ]
|
||||
then
|
||||
git config --global credential.helper store
|
||||
echo "https://${GH_TOKEN}:x-oauth-basic@github.com" >> ~/.git-credentials
|
||||
git config --global user.email "ci@bitwarden.com"
|
||||
git config --global user.name "Bitwarden CI"
|
||||
fi
|
||||
- cmd: choco install cloc --no-progress
|
||||
- cmd: "cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML"
|
||||
#- cmd: appveyor DownloadFile https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
|
||||
#- cmd: appveyor DownloadFile https://aka.ms/vs/15/release/vs_community.exe
|
||||
#- cmd: vs_community.exe update --wait --quiet --norestart --installPath "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community"
|
||||
#- cmd: ps: .\src\Android\update-android.ps1
|
||||
|
||||
before_build:
|
||||
- ps: |
|
||||
if($isWindows) {
|
||||
nuget restore
|
||||
if($env:UPLOAD_KEYSTORE_DEC_SECRET -or$env:KEYSTORE_DEC_SECRET -or $env:GOOGLE_SERVICES_DEC_SECRET -or $env:PLAY_DEC_SECRET) {
|
||||
nuget install secure-file -ExcludeVersion
|
||||
}
|
||||
if($env:GOOGLE_SERVICES_DEC_SECRET) {
|
||||
secure-file\tools\secure-file -decrypt src\Android\google-services.json.enc `
|
||||
-secret $env:GOOGLE_SERVICES_DEC_SECRET
|
||||
}
|
||||
}
|
||||
|
||||
build_script:
|
||||
- sh: |
|
||||
if [ "${APPVEYOR_REPO_TAG}" == "true" ]
|
||||
then
|
||||
mkdir dist
|
||||
cp CNAME ./dist
|
||||
cd store
|
||||
chmod 600 fdroid/config.py fdroid/keystore.jks
|
||||
mkdir -p temp/fdroid
|
||||
TEMP_DIR="$APPVEYOR_BUILD_FOLDER/store/temp/fdroid"
|
||||
cd fdroid
|
||||
echo "keypass=\"$FDROID_KEYSTORE_PASSWORD\"" >>config.py
|
||||
echo "keystorepass=\"$FDROID_KEYSTORE_PASSWORD\"" >>config.py
|
||||
echo "local_copy_dir=\"$TEMP_DIR\"" >>config.py
|
||||
mkdir -p repo
|
||||
curl -Lo repo/com.x8bit.bitwarden-fdroid.apk \
|
||||
https://github.com/bitwarden/mobile/releases/download/$APPVEYOR_REPO_TAG_NAME/com.x8bit.bitwarden-fdroid.apk
|
||||
fdroid update
|
||||
fdroid server update
|
||||
cd ..
|
||||
rm -rf temp/fdroid/archive
|
||||
mv -v temp/fdroid ../dist
|
||||
cd fdroid
|
||||
cp index.html btn.png qr.png ../../dist/fdroid
|
||||
cd $APPVEYOR_BUILD_FOLDER
|
||||
fi
|
||||
- ps: |
|
||||
if($isWindows -and $env:KEYSTORE_DEC_SECRET) {
|
||||
msbuild bitwarden-mobile.sln `
|
||||
"/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" `
|
||||
"/p:Configuration=Release"
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
.\src\Android\ci-build-apks.ps1
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden.aab
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden.apk
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden-fdroid.apk
|
||||
}
|
||||
|
||||
on_success:
|
||||
- sh: |
|
||||
if [ "${APPVEYOR_REPO_TAG}" == "true" -a "${GH_TOKEN}" != "" ]
|
||||
then
|
||||
npm run deploy
|
||||
fi
|
||||
- ps: |
|
||||
if($isWindows -and $env:PLAY_DEC_SECRET -and $env:APPVEYOR_REPO_BRANCH -eq 'master') {
|
||||
secure-file\tools\secure-file -decrypt store\google\Publisher\play_creds.json.enc -secret $env:PLAY_DEC_SECRET
|
||||
cd store\google\Publisher\bin\Release\netcoreapp2.0
|
||||
dotnet Publisher.dll `
|
||||
$env:APPVEYOR_BUILD_FOLDER\store\google\Publisher\play_creds.json `
|
||||
$env:APPVEYOR_BUILD_FOLDER\com.x8bit.bitwarden.aab `
|
||||
alpha
|
||||
cd $env:APPVEYOR_BUILD_FOLDER
|
||||
}
|
||||
|
||||
on_finish:
|
||||
- sh: |
|
||||
if [ "${DEBUG_SSH}" == "true" ]
|
||||
then
|
||||
export APPVEYOR_SSH_BLOCK=true
|
||||
curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
|
||||
fi
|
||||
- ps: |
|
||||
if($isWindows -and $env:DEBUG_RDP -eq "true") {
|
||||
$blockRdp = $true
|
||||
iex ((new-object net.webclient).DownloadString(`
|
||||
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
}
|
||||
|
||||
deploy:
|
||||
tag: $(APPVEYOR_REPO_TAG_NAME)
|
||||
release: $(RELEASE_NAME)
|
||||
provider: GitHub
|
||||
auth_token: $(GH_TOKEN)
|
||||
artifact: /.*/
|
||||
force_update: true
|
||||
on:
|
||||
branch: master
|
||||
APPVEYOR_REPO_TAG: true
|
||||
@@ -23,7 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
appveyor.yml = appveyor.yml
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
crowdin.yml = crowdin.yml
|
||||
|
||||
@@ -50,10 +50,15 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.ecosia.android", "url_bar"),
|
||||
new Browser("com.google.android.apps.chrome", "url_bar"),
|
||||
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
||||
new Browser("com.jamal2367.styx", "search"),
|
||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||
new Browser("com.microsoft.emmx", "url_bar"),
|
||||
new Browser("com.microsoft.emmx.beta", "url_bar"),
|
||||
new Browser("com.microsoft.emmx.canary", "url_bar"),
|
||||
new Browser("com.microsoft.emmx.dev", "url_bar"),
|
||||
new Browser("com.mmbox.browser", "search_box"),
|
||||
new Browser("com.mmbox.xbrowser", "search_box"),
|
||||
new Browser("com.mycompany.app.soulbrowser", "edit_text"),
|
||||
new Browser("com.naver.whale", "url_bar"),
|
||||
new Browser("com.opera.browser", "url_field"),
|
||||
new Browser("com.opera.browser.beta", "url_field"),
|
||||
@@ -77,6 +82,10 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("io.github.forkmaintainers.iceraven", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("mark.via", "am,an"),
|
||||
new Browser("mark.via.gp", "as"),
|
||||
new Browser("net.slions.fulguris.full.download", "search"),
|
||||
new Browser("net.slions.fulguris.full.download.debug", "search"),
|
||||
new Browser("net.slions.fulguris.full.playstore", "search"),
|
||||
new Browser("net.slions.fulguris.full.playstore.debug", "search"),
|
||||
new Browser("org.adblockplus.browser", "url_bar,url_bar_title"), // 2nd = Legacy (before v2)
|
||||
new Browser("org.adblockplus.browser.beta", "url_bar,url_bar_title"), // 2nd = Legacy (before v2)
|
||||
new Browser("org.bromite.bromite", "url_bar"),
|
||||
@@ -182,7 +191,9 @@ namespace Bit.Droid.Accessibility
|
||||
new KnownUsernameField("amazon.in", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.it", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.nl", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.pl", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.sa", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.se", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.sg", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
|
||||
// Amazon Web Services
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<Version>1.5.3.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>71.1740.0</Version>
|
||||
<Version>121.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.0.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.1.0" />
|
||||
@@ -90,6 +90,7 @@
|
||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.6" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.27.0" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>71.1600.0</Version>
|
||||
</PackageReference>
|
||||
@@ -113,7 +114,6 @@
|
||||
<Compile Include="Effects\FixedSizeEffect.cs" />
|
||||
<Compile Include="Effects\SelectableLabelEffect.cs" />
|
||||
<Compile Include="Effects\TabBarEffect.cs" />
|
||||
<Compile Include="Push\FirebaseInstanceIdService.cs" />
|
||||
<Compile Include="Push\FirebaseMessagingService.cs" />
|
||||
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\RestrictionsChangedReceiver.cs" />
|
||||
@@ -154,7 +154,6 @@
|
||||
<AndroidAsset Include="Assets\RobotoMono_Regular.ttf" />
|
||||
<AndroidAsset Include="Assets\MaterialIcons_Regular.ttf" />
|
||||
<None Include="8bit.keystore.enc" />
|
||||
<None Include="ci-build-apks.ps1" />
|
||||
<GoogleServicesJson Include="google-services.json" />
|
||||
<GoogleServicesJson Include="google-services.json.enc" />
|
||||
<None Include="fdroid-keystore.jks.enc" />
|
||||
|
||||
@@ -66,10 +66,15 @@ namespace Bit.Droid.Autofill
|
||||
"com.ecosia.android",
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
"com.jamal2367.styx",
|
||||
"com.kiwibrowser.browser",
|
||||
"com.microsoft.emmx",
|
||||
"com.microsoft.emmx.beta",
|
||||
"com.microsoft.emmx.canary",
|
||||
"com.microsoft.emmx.dev",
|
||||
"com.mmbox.browser",
|
||||
"com.mmbox.xbrowser",
|
||||
"com.mycompany.app.soulbrowser",
|
||||
"com.naver.whale",
|
||||
"com.opera.browser",
|
||||
"com.opera.browser.beta",
|
||||
@@ -92,6 +97,10 @@ namespace Bit.Droid.Autofill
|
||||
"io.github.forkmaintainers.iceraven",
|
||||
"mark.via",
|
||||
"mark.via.gp",
|
||||
"net.slions.fulguris.full.download",
|
||||
"net.slions.fulguris.full.download.debug",
|
||||
"net.slions.fulguris.full.playstore",
|
||||
"net.slions.fulguris.full.playstore.debug",
|
||||
"org.adblockplus.browser",
|
||||
"org.adblockplus.browser.beta",
|
||||
"org.bromite.bromite",
|
||||
|
||||
@@ -30,6 +30,16 @@ namespace Bit.Droid
|
||||
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
|
||||
ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
|
||||
ConfigChanges.Navigation)]
|
||||
[IntentFilter(
|
||||
new[] { Intent.ActionSend },
|
||||
Categories = new[] { Intent.CategoryDefault },
|
||||
DataMimeTypes = new[]
|
||||
{
|
||||
@"application/*",
|
||||
@"image/*",
|
||||
@"video/*",
|
||||
@"text/*"
|
||||
})]
|
||||
[Register("com.x8bit.bitwarden.MainActivity")]
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
{
|
||||
@@ -169,7 +179,7 @@ namespace Bit.Droid
|
||||
_appOptions.GeneratorTile = true;
|
||||
}
|
||||
}
|
||||
if (intent.GetBooleanExtra("myVaultTile", false))
|
||||
else if (intent.GetBooleanExtra("myVaultTile", false))
|
||||
{
|
||||
_messagingService.Send("popAllAndGoToTabMyVault");
|
||||
if (_appOptions != null)
|
||||
@@ -177,6 +187,14 @@ namespace Bit.Droid
|
||||
_appOptions.MyVaultTile = true;
|
||||
}
|
||||
}
|
||||
else if (intent.Action == Intent.ActionSend && intent.Type != null)
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.CreateSend = GetCreateSendRequest(intent);
|
||||
}
|
||||
_messagingService.Send("popAllAndGoToTabSend");
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseYubiKey(intent.DataString);
|
||||
@@ -298,7 +316,8 @@ namespace Bit.Droid
|
||||
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
|
||||
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
||||
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
||||
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false)
|
||||
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false),
|
||||
CreateSend = GetCreateSendRequest(Intent)
|
||||
};
|
||||
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
|
||||
if (fillType > 0)
|
||||
@@ -320,6 +339,37 @@ namespace Bit.Droid
|
||||
return options;
|
||||
}
|
||||
|
||||
private Tuple<SendType, string, byte[], string> GetCreateSendRequest(Intent intent)
|
||||
{
|
||||
if (intent.Action == Intent.ActionSend && intent.Type != null)
|
||||
{
|
||||
var type = intent.Type;
|
||||
if (type.Contains("text/"))
|
||||
{
|
||||
var subject = intent.GetStringExtra(Intent.ExtraSubject);
|
||||
var text = intent.GetStringExtra(Intent.ExtraText);
|
||||
return new Tuple<SendType, string, byte[], string>(SendType.Text, subject, null, text);
|
||||
}
|
||||
else
|
||||
{
|
||||
var data = intent.ClipData?.GetItemAt(0);
|
||||
var uri = data?.Uri;
|
||||
var filename = AndroidHelpers.GetFileName(ApplicationContext, uri);
|
||||
try
|
||||
{
|
||||
using (var stream = ContentResolver.OpenInputStream(uri))
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
stream.CopyTo(memoryStream);
|
||||
return new Tuple<SendType, string, byte[], string>(SendType.File, filename, memoryStream.ToArray(), null);
|
||||
}
|
||||
}
|
||||
catch (Java.IO.FileNotFoundException) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ParseYubiKey(string data)
|
||||
{
|
||||
if (data == null)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.9.1"
|
||||
android:versionName="2.10.0"
|
||||
android:installLocation="internalOnly"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
@@ -40,20 +40,6 @@
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
|
||||
android:exported="true"
|
||||
android:permission="com.google.android.c2dm.permission.SEND">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
||||
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
|
||||
<category android:name="com.x8bit.bitwarden" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#if !FDROID
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Firebase.Iid;
|
||||
|
||||
namespace Bit.Droid.Push
|
||||
{
|
||||
[Service]
|
||||
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
|
||||
public class FirebaseInstanceIdService : Firebase.Iid.FirebaseInstanceIdService
|
||||
{
|
||||
public async override void OnTokenRefresh()
|
||||
{
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
await storageService.SaveAsync(Constants.PushRegisteredTokenKey, FirebaseInstanceId.Instance.Token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,7 +1,7 @@
|
||||
#if !FDROID
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Firebase.Messaging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -10,10 +10,19 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.Droid.Push
|
||||
{
|
||||
[Service]
|
||||
[Service(Exported=false)]
|
||||
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
|
||||
public class FirebaseMessagingService : Firebase.Messaging.FirebaseMessagingService
|
||||
{
|
||||
public async override void OnNewToken(string token)
|
||||
{
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
|
||||
await storageService.SaveAsync(Core.Constants.PushRegisteredTokenKey, token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
}
|
||||
|
||||
public async override void OnMessageReceived(RemoteMessage message)
|
||||
{
|
||||
if (message?.Data == null)
|
||||
|
||||
@@ -65,18 +65,33 @@
|
||||
<compatibility-package
|
||||
android:name="com.google.android.apps.chrome_dev"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.jamal2367.styx"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.kiwibrowser.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.microsoft.emmx"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.microsoft.emmx.beta"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.microsoft.emmx.canary"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.microsoft.emmx.dev"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.mmbox.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.mmbox.xbrowser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.mycompany.app.soulbrowser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.naver.whale"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -143,6 +158,18 @@
|
||||
<compatibility-package
|
||||
android:name="mark.via.gp"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="net.slions.fulguris.full.download"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="net.slions.fulguris.full.download.debug"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="net.slions.fulguris.full.playstore"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="net.slions.fulguris.full.playstore.debug"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.adblockplus.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Javax.Crypto;
|
||||
using Javax.Crypto.Spec;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
@@ -33,5 +35,25 @@ namespace Bit.Droid.Services
|
||||
generator.Init(password, salt, iterations);
|
||||
return ((KeyParameter)generator.GenerateDerivedMacParameters(keySize)).GetKey();
|
||||
}
|
||||
|
||||
public byte[] AesGcmEncrypt(byte[] data, byte[] iv, byte[] key)
|
||||
{
|
||||
var secKey = new SecretKeySpec(key, "AES");
|
||||
var cipher = Cipher.GetInstance("AES/GCM/NoPadding");
|
||||
var gcmSpec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.EncryptMode, secKey, gcmSpec);
|
||||
var result = cipher.DoFinal(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] AesGcmDecrypt(byte[] data, byte[] iv, byte[] key)
|
||||
{
|
||||
var secKey = new SecretKeySpec(key, "AES");
|
||||
var cipher = Cipher.GetInstance("AES/GCM/NoPadding");
|
||||
var gcmSpec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.DecryptMode, secKey, gcmSpec);
|
||||
var result = cipher.DoFinal(data);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,6 +760,17 @@ namespace Bit.Droid.Services
|
||||
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
|
||||
return SystemClock.ElapsedRealtime();
|
||||
}
|
||||
|
||||
public void CloseMainApp()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
activity.Finish();
|
||||
_messagingService.Send("finishMainActivity");
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
$rootPath = $env:APPVEYOR_BUILD_FOLDER;
|
||||
|
||||
$androidPath = $($rootPath + "\src\Android\Android.csproj");
|
||||
$appPath = $($rootPath + "\src\App\App.csproj");
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Increment Version"
|
||||
echo "########################################"
|
||||
|
||||
$androidManifest = $($rootPath + "\src\Android\Properties\AndroidManifest.xml");
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
|
||||
$node=$xml.SelectNodes("/manifest");
|
||||
$node.SetAttribute("android:versionCode", $env:APPVEYOR_BUILD_NUMBER);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Decrypt Keystore"
|
||||
echo "########################################"
|
||||
|
||||
$encKeystorePath = $($rootPath + "\src\Android\8bit.keystore.enc");
|
||||
$encFdroidKeystorePath = $($rootPath + "\src\Android\fdroid-keystore.jks.enc");
|
||||
$encUploadKeystorePath = $($rootPath + "\src\Android\upload-keystore.jks.enc");
|
||||
$secureFilePath = $($rootPath + "\secure-file\tools\secure-file.exe");
|
||||
|
||||
Invoke-Expression "& `"$secureFilePath`" -decrypt $($encKeystorePath) -secret $($env:keystore_dec_secret)"
|
||||
Invoke-Expression "& `"$secureFilePath`" -decrypt $($encFdroidKeystorePath) -secret $($env:fdroid_apk_keystore_dec_secret)"
|
||||
Invoke-Expression "& `"$secureFilePath`" -decrypt $($encUploadKeystorePath) -secret $($env:upload_keystore_dec_secret)"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Sign Google Play Bundle Release Configuration"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=upload" "/p:AndroidSigningKeyPass=$($env:upload_keystore_password)" `
|
||||
"/p:AndroidSigningKeyStore=upload-keystore.jks" "/p:AndroidSigningStorePass=$($env:upload_keystore_password)" `
|
||||
"/p:AndroidPackageFormat=aab" "/v:quiet"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Copy Google Play Bundle to project root"
|
||||
echo "########################################"
|
||||
|
||||
$signedAabPath = $($rootPath + "\src\Android\bin\Release\com.x8bit.bitwarden-Signed.aab");
|
||||
$signedAabDestPath = $($rootPath + "\com.x8bit.bitwarden.aab");
|
||||
|
||||
Copy-Item $signedAabPath $signedAabDestPath
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Sign APK Release Configuration"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:keystore_password)" `
|
||||
"/p:AndroidSigningKeyStore=8bit.keystore" "/p:AndroidSigningStorePass=$($env:keystore_password)" "/v:quiet"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Copy Release APK to project root"
|
||||
echo "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "\src\Android\bin\Release\com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "\com.x8bit.bitwarden.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Clean Android and App"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
msbuild "$($appPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Backup project files"
|
||||
echo "########################################"
|
||||
|
||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||
Copy-Item $androidPath $($androidPath + ".original");
|
||||
Copy-Item $appPath $($appPath + ".original");
|
||||
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Cleanup Android Manifest"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
|
||||
$nsAndroid=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$nsAndroid.AddNamespace("android", "http://schemas.android.com/apk/res/android");
|
||||
|
||||
$firebaseReceiver1=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdInternalReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver1.ParentNode.RemoveChild($firebaseReceiver1);
|
||||
|
||||
$firebaseReceiver2=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver2.ParentNode.RemoveChild($firebaseReceiver2);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Uninstall from Android.csproj"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidPath);
|
||||
|
||||
$ns=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI);
|
||||
|
||||
$firebaseNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns);
|
||||
$firebaseNode.ParentNode.RemoveChild($firebaseNode);
|
||||
|
||||
$safetyNetNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.GooglePlayServices.SafetyNet']", $ns);
|
||||
$safetyNetNode.ParentNode.RemoveChild($safetyNetNode);
|
||||
|
||||
$xml.Save($androidPath);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Uninstall from App.csproj"
|
||||
echo "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($appPath);
|
||||
|
||||
$appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']");
|
||||
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
|
||||
|
||||
$xml.Save($appPath);
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Restore NuGet"
|
||||
echo "########################################"
|
||||
|
||||
Invoke-Expression "& nuget restore"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Build and Sign FDroid Configuration"
|
||||
echo "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" `
|
||||
"/p:Configuration=FDroid"
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=FDroid" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:fdroid_apk_keystore_password)" `
|
||||
"/p:AndroidSigningKeyStore=fdroid-keystore.jks" "/p:AndroidSigningStorePass=$($env:fdroid_apk_keystore_password)" `
|
||||
"/v:quiet"
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Copy FDroid apk to project root"
|
||||
echo "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "\src\Android\bin\FDroid\com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "\com.x8bit.bitwarden-fdroid.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Done"
|
||||
echo "########################################"
|
||||
@@ -44,5 +44,6 @@ namespace Bit.App.Abstractions
|
||||
void OpenAutofillSettings();
|
||||
bool UsingDarkTheme();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ namespace Bit.App
|
||||
await SetMainPageAsync();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
||||
message.Command == "popAllAndGoToTabMyVault")
|
||||
message.Command == "popAllAndGoToTabMyVault" ||
|
||||
message.Command == "popAllAndGoToTabSend")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
@@ -146,11 +147,15 @@ namespace Bit.App
|
||||
Options.MyVaultTile = false;
|
||||
tabsPage.ResetToVaultPage();
|
||||
}
|
||||
else
|
||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
||||
{
|
||||
Options.GeneratorTile = false;
|
||||
tabsPage.ResetToGeneratorPage();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabSend")
|
||||
{
|
||||
tabsPage.ResetToSendPage();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -244,7 +249,8 @@ namespace Bit.App
|
||||
_collectionService.ClearAsync(userId),
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_vaultTimeoutService.ClearAsync(),
|
||||
_stateService.PurgeAsync());
|
||||
_stateService.PurgeAsync(),
|
||||
_deviceActionService.ClearCacheAsync());
|
||||
_vaultTimeoutService.BiometricLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
_authService.LogOut(() =>
|
||||
@@ -274,6 +280,10 @@ namespace Bit.App
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
}
|
||||
else if (Options.CreateSend != null)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new TabsPage(Options);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
@@ -19,6 +20,7 @@ namespace Bit.App.Models
|
||||
public string SaveCardExpYear { get; set; }
|
||||
public string SaveCardCode { get; set; }
|
||||
public bool IosExtension { get; set; }
|
||||
public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
|
||||
|
||||
public void SetAllFrom(AppOptions o)
|
||||
{
|
||||
@@ -41,6 +43,7 @@ namespace Bit.App.Models
|
||||
SaveCardExpYear = o.SaveCardExpYear;
|
||||
SaveCardCode = o.SaveCardCode;
|
||||
IosExtension = o.IosExtension;
|
||||
CreateSend = o.CreateSend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutService.PinProtectedKey);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
failed = decPin != Pin;
|
||||
if (!failed)
|
||||
{
|
||||
@@ -272,7 +272,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
_vaultTimeoutService.PinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Bit.App.Pages
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
|
||||
Tuple<SymmetricCryptoKey, CipherString> encKey;
|
||||
Tuple<SymmetricCryptoKey, EncString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
if (existingEncKey == null)
|
||||
{
|
||||
|
||||
@@ -89,6 +89,18 @@
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
@@ -105,7 +117,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
|
||||
IsVisible="{Binding ShowTypeButtons}">
|
||||
<Label
|
||||
Text="{u:I18n Type}"
|
||||
StyleClass="box-label" />
|
||||
@@ -210,6 +222,7 @@
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Button
|
||||
Text="{u:I18n ChooseFile}"
|
||||
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked" />
|
||||
@@ -222,7 +235,7 @@
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n TypeFileInfo}"
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
IsVisible="{Binding ShowTypeButtons}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
@@ -490,6 +503,20 @@
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideEmail}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
@@ -513,4 +540,4 @@
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
@@ -15,18 +17,21 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public SendAddEditPage(
|
||||
AppOptions appOptions = null,
|
||||
string sendId = null,
|
||||
SendType? type = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SendId = sendId;
|
||||
_vm.Type = type;
|
||||
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
|
||||
SetActivityIndicator();
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
@@ -95,6 +100,7 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
return;
|
||||
}
|
||||
await HandleCreateRequest();
|
||||
if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
@@ -271,5 +277,33 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Remove(_shareLink);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCreateRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vm.IsAddFromShare = true;
|
||||
|
||||
var name = _appOptions.CreateSend.Item2;
|
||||
_vm.Send.Name = name;
|
||||
|
||||
var type = _appOptions.CreateSend.Item1;
|
||||
if (type == SendType.File)
|
||||
{
|
||||
_vm.FileData = _appOptions.CreateSend.Item3;
|
||||
_vm.FileName = name;
|
||||
FileType_Clicked(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var text = _appOptions.CreateSend.Item4;
|
||||
_vm.Send.Text.Text = text;
|
||||
TextType_Clicked(null, null);
|
||||
}
|
||||
_appOptions.CreateSend = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Bit.App.Pages
|
||||
private readonly ISendService _sendService;
|
||||
private bool _sendEnabled;
|
||||
private bool _canAccessPremium;
|
||||
private bool _emailVerified;
|
||||
private SendView _send;
|
||||
private string _fileName;
|
||||
private bool _showOptions;
|
||||
@@ -42,6 +43,9 @@ namespace Bit.App.Pages
|
||||
nameof(IsText),
|
||||
nameof(IsFile),
|
||||
};
|
||||
private bool _disableHideEmail;
|
||||
private bool _sendOptionsPolicyInEffect;
|
||||
private bool _disableHideEmailControl;
|
||||
|
||||
public SendAddEditPageViewModel()
|
||||
{
|
||||
@@ -91,6 +95,8 @@ namespace Bit.App.Pages
|
||||
public byte[] FileData { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
public bool ShareOnSave { get; set; }
|
||||
public bool DisableHideEmailControl { get; set; }
|
||||
public bool IsAddFromShare { get; set; }
|
||||
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
|
||||
@@ -194,6 +200,17 @@ namespace Bit.App.Pages
|
||||
nameof(ShowPasswordIcon)
|
||||
});
|
||||
}
|
||||
public bool DisableHideEmail
|
||||
{
|
||||
get => _disableHideEmail;
|
||||
set => SetProperty(ref _disableHideEmail, value);
|
||||
}
|
||||
public bool SendOptionsPolicyInEffect
|
||||
{
|
||||
get => _sendOptionsPolicyInEffect;
|
||||
set => SetProperty(ref _sendOptionsPolicyInEffect, value);
|
||||
}
|
||||
public bool ShowTypeButtons => !EditMode && !IsAddFromShare;
|
||||
public bool EditMode => !string.IsNullOrWhiteSpace(SendId);
|
||||
public bool IsText => Send?.Type == SendType.Text;
|
||||
public bool IsFile => Send?.Type == SendType.File;
|
||||
@@ -205,7 +222,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
PageTitle = EditMode ? AppResources.EditSend : AppResources.AddSend;
|
||||
_canAccessPremium = await _userService.CanAccessPremiumAsync();
|
||||
_emailVerified = await _userService.GetEmailVerifiedAsync();
|
||||
SendEnabled = ! await AppHelpers.IsSendDisabledByPolicyAsync();
|
||||
DisableHideEmail = await AppHelpers.IsHideEmailDisabledByPolicyAsync();
|
||||
SendOptionsPolicyInEffect = SendEnabled && DisableHideEmail;
|
||||
}
|
||||
|
||||
public async Task<bool> LoadAsync()
|
||||
@@ -228,7 +248,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultType = _canAccessPremium ? SendType.File : SendType.Text;
|
||||
var defaultType = _canAccessPremium && _emailVerified ? SendType.File : SendType.Text;
|
||||
Send = new SendView
|
||||
{
|
||||
Type = Type.GetValueOrDefault(defaultType),
|
||||
@@ -243,6 +263,10 @@ namespace Bit.App.Pages
|
||||
_isOverridingPickers = false;
|
||||
}
|
||||
|
||||
DisableHideEmailControl = !SendEnabled ||
|
||||
(!EditMode && DisableHideEmail) ||
|
||||
(EditMode && DisableHideEmail && !Send.HideEmail);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -315,7 +339,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (!_canAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
|
||||
return false;
|
||||
}
|
||||
if (!_emailVerified)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFileEmailVerificationRequired);
|
||||
return false;
|
||||
}
|
||||
if (!EditMode)
|
||||
@@ -354,10 +383,6 @@ namespace Bit.App.Pages
|
||||
var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
|
||||
if (Device.RuntimePlatform == Device.Android && IsFile)
|
||||
{
|
||||
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/5418
|
||||
@@ -366,6 +391,21 @@ namespace Bit.App.Pages
|
||||
_messagingService.Send("sendUpdated");
|
||||
}
|
||||
|
||||
if (!ShareOnSave)
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
|
||||
}
|
||||
|
||||
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_deviceActionService.CloseMainApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
if (ShareOnSave)
|
||||
{
|
||||
var savedSend = await _sendService.GetAsync(sendId);
|
||||
@@ -375,7 +415,7 @@ namespace Bit.App.Pages
|
||||
await AppHelpers.ShareSendUrlAsync(savedSendView);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
@@ -412,11 +452,37 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task TypeChangedAsync(SendType type)
|
||||
{
|
||||
if (!SendEnabled)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendDisabledWarning);
|
||||
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_deviceActionService.CloseMainApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Send != null)
|
||||
{
|
||||
if (!EditMode && type == SendType.File && !_canAccessPremium)
|
||||
if (!EditMode && type == SendType.File && (!_canAccessPremium || !_emailVerified))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
if (!_canAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
|
||||
}
|
||||
else if (!_emailVerified)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFileEmailVerificationRequired);
|
||||
}
|
||||
|
||||
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_deviceActionService.CloseMainApp();
|
||||
return;
|
||||
}
|
||||
type = SendType.Text;
|
||||
}
|
||||
Send.Type = type;
|
||||
|
||||
@@ -19,10 +19,11 @@ namespace Bit.App.Pages
|
||||
private readonly SendGroupingsPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private PreviousPageInfo _previousPage;
|
||||
|
||||
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
|
||||
PreviousPageInfo previousPage = null)
|
||||
AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
@@ -35,6 +36,7 @@ namespace Bit.App.Pages
|
||||
_vm.Page = this;
|
||||
_vm.MainPage = mainPage;
|
||||
_vm.Type = type;
|
||||
_appOptions = appOptions;
|
||||
_previousPage = previousPage;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
@@ -109,8 +111,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
await ShowPreviousPageAsync();
|
||||
AdjustToolbar();
|
||||
await CheckAddRequest();
|
||||
}, _mainContent);
|
||||
}
|
||||
|
||||
@@ -122,6 +124,18 @@ namespace Bit.App.Pages
|
||||
_vm.DisableRefreshing();
|
||||
}
|
||||
|
||||
private async Task CheckAddRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend != null)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new SendAddEditPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
@@ -172,28 +186,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new SendAddEditPage(null, _vm.Type);
|
||||
var page = new SendAddEditPage(null, null, _vm.Type);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPreviousPageAsync()
|
||||
{
|
||||
if (_previousPage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.SendId)));
|
||||
}
|
||||
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.SendId)));
|
||||
}
|
||||
_previousPage = null;
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_addItem.IsEnabled = _vm.SendEnabled;
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SelectSendAsync(SendView send)
|
||||
{
|
||||
var page = new SendAddEditPage(send.Id);
|
||||
var page = new SendAddEditPage(null, send.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SelectSendAsync(SendView send)
|
||||
{
|
||||
var page = new SendAddEditPage(send.Id);
|
||||
var page = new SendAddEditPage(null, send.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Bit.App.Pages
|
||||
};
|
||||
Children.Add(_groupingsPage);
|
||||
|
||||
_sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true))
|
||||
_sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true, null, null, appOptions))
|
||||
{
|
||||
Title = AppResources.Send,
|
||||
IconImageSource = "paper_plane.png",
|
||||
@@ -60,6 +60,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
appOptions.MyVaultTile = false;
|
||||
}
|
||||
else if (appOptions?.CreateSend != null)
|
||||
{
|
||||
ResetToSendPage();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetToVaultPage()
|
||||
@@ -71,6 +75,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
CurrentPage = _generatorPage;
|
||||
}
|
||||
|
||||
public void ResetToSendPage()
|
||||
{
|
||||
CurrentPage = _sendGroupingsPage;
|
||||
}
|
||||
|
||||
protected async override void OnCurrentPageChanged()
|
||||
{
|
||||
|
||||
@@ -477,7 +477,7 @@ namespace Bit.App.Pages
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
try
|
||||
{
|
||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId);
|
||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (data == null)
|
||||
{
|
||||
|
||||
24
src/App/Resources/AppResources.Designer.cs
generated
24
src/App/Resources/AppResources.Designer.cs
generated
@@ -3484,5 +3484,29 @@ namespace Bit.App.Resources {
|
||||
return ResourceManager.GetString("AboutSend", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HideEmail {
|
||||
get {
|
||||
return ResourceManager.GetString("HideEmail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SendOptionsPolicyInEffect {
|
||||
get {
|
||||
return ResourceManager.GetString("SendOptionsPolicyInEffect", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SendFilePremiumRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("SendFilePremiumRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SendFileEmailVerificationRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("SendFileEmailVerificationRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1972,4 +1972,19 @@
|
||||
<value>About Send</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="HideEmail" xml:space="preserve">
|
||||
<value>Hide my email address from recipients.</value>
|
||||
</data>
|
||||
<data name="SendOptionsPolicyInEffect" xml:space="preserve">
|
||||
<value>One or more organization policies are affecting your Send options.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SendFilePremiumRequired" xml:space="preserve">
|
||||
<value>Free accounts are restricted to sharing text only. A premium membership is required to use files with Send.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SendFileEmailVerificationRequired" xml:space="preserve">
|
||||
<value>You must verify your email to use files with Send.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace Bit.App.Utilities
|
||||
}
|
||||
else if (selection == AppResources.Edit)
|
||||
{
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(send.Id)));
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(null, send.Id)));
|
||||
}
|
||||
else if (selection == AppResources.CopyLink)
|
||||
{
|
||||
@@ -311,6 +311,26 @@ namespace Bit.App.Utilities
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<bool> IsHideEmailDisabledByPolicyAsync()
|
||||
{
|
||||
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
|
||||
var policies = await policyService.GetAll(PolicyType.SendOptions);
|
||||
var organizations = await userService.GetAllOrganizationAsync();
|
||||
return organizations.Any(o =>
|
||||
{
|
||||
return o.Enabled &&
|
||||
o.Status == OrganizationUserStatusType.Confirmed &&
|
||||
o.UsePolicies &&
|
||||
!o.canManagePolicies &&
|
||||
policies.Any(p => p.OrganizationId == o.Id &&
|
||||
p.Enabled &&
|
||||
p.Data.ContainsKey("disableHideEmail") &&
|
||||
(bool)p.Data["disableHideEmail"]);
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService,
|
||||
IDeviceActionService deviceActionService, IStorageService storageService)
|
||||
{
|
||||
@@ -389,6 +409,11 @@ namespace Bit.App.Utilities
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
|
||||
return true;
|
||||
}
|
||||
if (appOptions.CreateSend != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new SendAddEditPage(appOptions));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,14 @@ namespace Bit.Core.Abstractions
|
||||
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
||||
TRequest body, bool authed, bool hasResponse);
|
||||
void SetUrls(EnvironmentUrls urls);
|
||||
Task<CipherResponse> PostCipherAttachmentAsync(string id, MultipartFormDataContent data);
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
Task<CipherResponse> PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data);
|
||||
Task<AttachmentUploadDataResponse> PostCipherAttachmentAsync(string id, AttachmentRequest request);
|
||||
Task<AttachmentResponse> GetAttachmentData(string cipherId, string attachmentId);
|
||||
Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data,
|
||||
string organizationId);
|
||||
Task<AttachmentUploadDataResponse> RenewAttachmentUploadUrlAsync(string id, string attachmentId);
|
||||
Task PostAttachmentFileAsync(string id, string attachmentId, MultipartFormDataContent data);
|
||||
Task<List<BreachAccountResponse>> GetHibpBreachAsync(string username);
|
||||
Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request);
|
||||
Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request);
|
||||
@@ -56,7 +61,11 @@ namespace Bit.Core.Abstractions
|
||||
|
||||
Task<SendResponse> GetSendAsync(string id);
|
||||
Task<SendResponse> PostSendAsync(SendRequest request);
|
||||
Task<SendFileUploadDataResponse> PostFileTypeSendAsync(SendRequest request);
|
||||
Task PostSendFileAsync(string sendId, string fileId, MultipartFormDataContent data);
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
Task<SendResponse> PostSendFileAsync(MultipartFormDataContent data);
|
||||
Task<SendFileUploadDataResponse> RenewFileUploadUrlAsync(string sendId, string fileId);
|
||||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||
Task DeleteSendAsync(string id);
|
||||
|
||||
11
src/Core/Abstractions/IAzureFileUpoadService.cs
Normal file
11
src/Core/Abstractions/IAzureFileUpoadService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IAzureFileUploadService
|
||||
{
|
||||
Task Upload(string uri, EncByteArray data, Func<Task<string>> renewalCallback);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace Bit.Core.Abstractions
|
||||
Task UpdateLastUsedDateAsync(string id);
|
||||
Task UpsertAsync(CipherData cipher);
|
||||
Task UpsertAsync(List<CipherData> cipher);
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||
Task SoftDeleteWithServerAsync(string id);
|
||||
Task RestoreWithServerAsync(string id);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> HashAsync(byte[] value, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm);
|
||||
Task<bool> CompareAsync(byte[] a, byte[] b);
|
||||
Task<byte[]> AesEncryptAsync(byte[] data, byte[] iv, byte[] key);
|
||||
Task<byte[]> AesDecryptAsync(byte[] data, byte[] iv, byte[] key);
|
||||
Task<byte[]> AesEncryptAsync(byte[] data, byte[] iv, byte[] key, AesMode mode);
|
||||
Task<byte[]> AesDecryptAsync(byte[] data, byte[] iv, byte[] key, AesMode mode);
|
||||
Task<byte[]> RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> RsaExtractPublicKeyAsync(byte[] privateKey);
|
||||
|
||||
@@ -5,5 +5,7 @@ namespace Bit.Core.Abstractions
|
||||
public interface ICryptoPrimitiveService
|
||||
{
|
||||
byte[] Pbkdf2(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations);
|
||||
byte[] AesGcmEncrypt(byte[] data, byte[] iv, byte[] key);
|
||||
byte[] AesGcmDecrypt(byte[] data, byte[] iv, byte[] key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ namespace Bit.Core.Abstractions
|
||||
Task ClearOrgKeysAsync(bool memoryOnly = false);
|
||||
Task ClearPinProtectedKeyAsync();
|
||||
Task<byte[]> DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key);
|
||||
Task<byte[]> DecryptToBytesAsync(CipherString cipherString, SymmetricCryptoKey key = null);
|
||||
Task<string> DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null);
|
||||
Task<CipherString> EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<CipherString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null);
|
||||
Task<byte[]> EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<byte[]> DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null);
|
||||
Task<string> DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null);
|
||||
Task<EncString> EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<EncString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null);
|
||||
Task<EncByteArray> EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<SymmetricCryptoKey> GetEncKeyAsync(SymmetricCryptoKey key = null);
|
||||
Task<List<string>> GetFingerprintAsync(string userId, byte[] publicKey = null);
|
||||
Task<SymmetricCryptoKey> GetKeyAsync();
|
||||
@@ -33,17 +33,17 @@ namespace Bit.Core.Abstractions
|
||||
Task<bool> HasEncKeyAsync();
|
||||
Task<string> HashPasswordAsync(string password, SymmetricCryptoKey key);
|
||||
Task<bool> HasKeyAsync();
|
||||
Task<Tuple<SymmetricCryptoKey, CipherString>> MakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<Tuple<SymmetricCryptoKey, EncString>> MakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<SymmetricCryptoKey> MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations);
|
||||
Task<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations,
|
||||
CipherString protectedKeyCs = null);
|
||||
Task<Tuple<string, CipherString>> MakeKeyPairAsync(SymmetricCryptoKey key = null);
|
||||
EncString protectedKeyEs = null);
|
||||
Task<Tuple<string, EncString>> MakeKeyPairAsync(SymmetricCryptoKey key = null);
|
||||
Task<SymmetricCryptoKey> MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations);
|
||||
Task<Tuple<CipherString, SymmetricCryptoKey>> MakeShareKeyAsync();
|
||||
Task<Tuple<EncString, SymmetricCryptoKey>> MakeShareKeyAsync();
|
||||
Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial);
|
||||
Task<int> RandomNumberAsync(int min, int max);
|
||||
Task<Tuple<SymmetricCryptoKey, CipherString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<CipherString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
|
||||
Task<Tuple<SymmetricCryptoKey, EncString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
|
||||
Task SetEncKeyAsync(string encKey);
|
||||
Task SetEncPrivateKeyAsync(string encPrivateKey);
|
||||
Task SetKeyAsync(SymmetricCryptoKey key);
|
||||
|
||||
10
src/Core/Abstractions/IFileUploadService.cs
Normal file
10
src/Core/Abstractions/IFileUploadService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
|
||||
namespace Bit.Core.Abstractions {
|
||||
public interface IFileUploadService {
|
||||
Task UploadCipherAttachmentFileAsync(AttachmentUploadDataResponse uploadData, string fileName, EncByteArray encryptedFileData);
|
||||
Task UploadSendFileAsync(SendFileUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ namespace Bit.Core.Abstractions
|
||||
public interface ISendService
|
||||
{
|
||||
void ClearCache();
|
||||
Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
|
||||
Task<(Send send, EncByteArray encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
|
||||
SymmetricCryptoKey key = null);
|
||||
Task<Send> GetAsync(string id);
|
||||
Task<List<Send>> GetAllAsync();
|
||||
Task<List<SendView>> GetAllDecryptedAsync();
|
||||
Task<string> SaveWithServerAsync(Send sendData, byte[] encryptedFileData);
|
||||
Task<string> SaveWithServerAsync(Send sendData, EncByteArray encryptedFileData);
|
||||
Task UpsertAsync(params SendData[] send);
|
||||
Task ReplaceAsync(Dictionary<string, SendData> sends);
|
||||
Task ClearAsync(string userId);
|
||||
|
||||
@@ -17,10 +17,12 @@ namespace Bit.Core.Abstractions
|
||||
Task<int?> GetKdfIterationsAsync();
|
||||
Task<Organization> GetOrganizationAsync(string id);
|
||||
Task<string> GetSecurityStampAsync();
|
||||
Task<bool> GetEmailVerifiedAsync();
|
||||
Task<string> GetUserIdAsync();
|
||||
Task<bool> IsAuthenticatedAsync();
|
||||
Task ReplaceOrganizationsAsync(Dictionary<string, OrganizationData> organizations);
|
||||
Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations);
|
||||
Task SetSecurityStampAsync(string stamp);
|
||||
Task SetEmailVerifiedAsync(bool emailVerified);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IVaultTimeoutService
|
||||
{
|
||||
CipherString PinProtectedKey { get; set; }
|
||||
EncString PinProtectedKey { get; set; }
|
||||
bool BiometricLocked { get; set; }
|
||||
|
||||
Task CheckVaultTimeoutAsync();
|
||||
|
||||
8
src/Core/Enums/AesMode.cs
Normal file
8
src/Core/Enums/AesMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum AesMode : byte
|
||||
{
|
||||
CBC = 0,
|
||||
GCM = 1,
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
Rsa2048_OaepSha256_B64 = 3,
|
||||
Rsa2048_OaepSha1_B64 = 4,
|
||||
Rsa2048_OaepSha256_HmacSha256_B64 = 5,
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
|
||||
AesGcm256_B64 = 7
|
||||
}
|
||||
}
|
||||
|
||||
9
src/Core/Enums/FileUploadType.cs
Normal file
9
src/Core/Enums/FileUploadType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum FileUploadType
|
||||
{
|
||||
Direct = 0,
|
||||
Azure = 1,
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@
|
||||
RequireSso = 4, // Requires users to authenticate with SSO
|
||||
PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items
|
||||
DisableSend = 6, // Disables the ability to create and edit Sends
|
||||
SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Bit.Core.Models.Data
|
||||
DeletionDate = response.DeletionDate;
|
||||
Password = response.Password;
|
||||
Disabled = response.Disabled;
|
||||
HideEmail = response.HideEmail.GetValueOrDefault();
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
@@ -54,5 +55,6 @@ namespace Bit.Core.Models.Data
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool HideEmail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ namespace Bit.Core.Models.Domain
|
||||
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 EncString Key { get; set; }
|
||||
public EncString FileName { get; set; }
|
||||
|
||||
public async Task<AttachmentView> DecryptAsync(string orgId)
|
||||
{
|
||||
|
||||
@@ -24,12 +24,12 @@ namespace Bit.Core.Models.Domain
|
||||
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 EncString CardholderName { get; set; }
|
||||
public EncString Brand { get; set; }
|
||||
public EncString Number { get; set; }
|
||||
public EncString ExpMonth { get; set; }
|
||||
public EncString ExpYear { get; set; }
|
||||
public EncString Code { get; set; }
|
||||
|
||||
public Task<CardView> DecryptAsync(string orgId)
|
||||
{
|
||||
|
||||
@@ -58,8 +58,8 @@ namespace Bit.Core.Models.Domain
|
||||
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 EncString Name { get; set; }
|
||||
public EncString Notes { get; set; }
|
||||
public Enums.CipherType Type { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Bit.Core.Models.Domain
|
||||
|
||||
public string Id { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
public EncString Name { get; set; }
|
||||
public string ExternalId { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ namespace Bit.Core.Models.Domain
|
||||
else
|
||||
{
|
||||
domainPropInfo.SetValue(domain,
|
||||
dataObjProp != null ? new CipherString(dataObjProp as string) : null, null);
|
||||
dataObjProp != null ? new EncString(dataObjProp as string) : null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void BuildDataModel<D, O>(D domain, O dataObj, HashSet<string> map,
|
||||
HashSet<string> notCipherStringList = null)
|
||||
HashSet<string> notEncryptedStringList = null)
|
||||
where D : Domain
|
||||
where O : Data.Data
|
||||
{
|
||||
@@ -41,13 +41,13 @@ namespace Bit.Core.Models.Domain
|
||||
var domainPropInfo = domainType.GetProperty(prop);
|
||||
var domainProp = domainPropInfo.GetValue(domain);
|
||||
var dataObjPropInfo = dataObjType.GetProperty(prop);
|
||||
if (notCipherStringList?.Contains(prop) ?? false)
|
||||
if (notEncryptedStringList?.Contains(prop) ?? false)
|
||||
{
|
||||
dataObjPropInfo.SetValue(dataObj, domainProp, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
dataObjPropInfo.SetValue(dataObj, (domainProp as CipherString)?.EncryptedString, null);
|
||||
dataObjPropInfo.SetValue(dataObj, (domainProp as EncString)?.EncryptedString, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Bit.Core.Models.Domain
|
||||
{
|
||||
var domainPropInfo = domainType.GetProperty(propName);
|
||||
string val = null;
|
||||
if (domainPropInfo.GetValue(domain) is CipherString domainProp)
|
||||
if (domainPropInfo.GetValue(domain) is EncString domainProp)
|
||||
{
|
||||
val = await domainProp.DecryptAsync(orgId, key);
|
||||
}
|
||||
|
||||
12
src/Core/Models/Domain/EncByteArray.cs
Normal file
12
src/Core/Models/Domain/EncByteArray.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class EncByteArray
|
||||
{
|
||||
public byte[] Buffer { get; }
|
||||
|
||||
public EncByteArray(byte[] encryptedByteArray)
|
||||
{
|
||||
Buffer = encryptedByteArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class CipherString
|
||||
public class EncString
|
||||
{
|
||||
private string _decryptedValue;
|
||||
|
||||
public CipherString(EncryptionType encryptionType, string data, string iv = null, string mac = null)
|
||||
public EncString(EncryptionType encryptionType, string data, string iv = null, string mac = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data))
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Bit.Core.Models.Domain
|
||||
Mac = mac;
|
||||
}
|
||||
|
||||
public CipherString(string encryptedString)
|
||||
public EncString(string encryptedString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(encryptedString))
|
||||
{
|
||||
@@ -73,6 +73,7 @@ namespace Bit.Core.Models.Domain
|
||||
Mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
case EncryptionType.AesGcm256_B64:
|
||||
if (encPieces.Length != 2)
|
||||
{
|
||||
return;
|
||||
@@ -22,8 +22,8 @@ namespace Bit.Core.Models.Domain
|
||||
BuildDomainModel(this, obj, _map, alreadyEncrypted);
|
||||
}
|
||||
|
||||
public CipherString Name { get; set; }
|
||||
public CipherString Value { get; set; }
|
||||
public EncString Name { get; set; }
|
||||
public EncString Value { get; set; }
|
||||
public FieldType Type { get; set; }
|
||||
|
||||
public Task<FieldView> DecryptAsync(string orgId)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
public EncString Name { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public Task<FolderView> DecryptAsync()
|
||||
|
||||
@@ -36,24 +36,24 @@ namespace Bit.Core.Models.Domain
|
||||
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 EncString Title { get; set; }
|
||||
public EncString FirstName { get; set; }
|
||||
public EncString MiddleName { get; set; }
|
||||
public EncString LastName { get; set; }
|
||||
public EncString Address1 { get; set; }
|
||||
public EncString Address2 { get; set; }
|
||||
public EncString Address3 { get; set; }
|
||||
public EncString City { get; set; }
|
||||
public EncString State { get; set; }
|
||||
public EncString PostalCode { get; set; }
|
||||
public EncString Country { get; set; }
|
||||
public EncString Company { get; set; }
|
||||
public EncString Email { get; set; }
|
||||
public EncString Phone { get; set; }
|
||||
public EncString SSN { get; set; }
|
||||
public EncString Username { get; set; }
|
||||
public EncString PassportNumber { get; set; }
|
||||
public EncString LicenseNumber { get; set; }
|
||||
|
||||
public Task<IdentityView> DecryptAsync(string orgId)
|
||||
{
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
|
||||
public List<LoginUri> Uris { get; set; }
|
||||
public CipherString Username { get; set; }
|
||||
public CipherString Password { get; set; }
|
||||
public EncString Username { get; set; }
|
||||
public EncString Password { get; set; }
|
||||
public DateTime? PasswordRevisionDate { get; set; }
|
||||
public CipherString Totp { get; set; }
|
||||
public EncString Totp { get; set; }
|
||||
|
||||
public async Task<LoginView> DecryptAsync(string orgId)
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Bit.Core.Models.Domain
|
||||
BuildDomainModel(this, obj, _map, alreadyEncrypted);
|
||||
}
|
||||
|
||||
public CipherString Uri { get; set; }
|
||||
public EncString Uri { get; set; }
|
||||
public UriMatchType? Match { get; set; }
|
||||
|
||||
public Task<LoginUriView> DecryptAsync(string orgId)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Bit.Core.Models.Domain
|
||||
LastUsedDate = obj.LastUsedDate.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public CipherString Password { get; set; }
|
||||
public EncString Password { get; set; }
|
||||
public DateTime LastUsedDate { get; set; }
|
||||
|
||||
public Task<PasswordHistoryView> DecryptAsync(string orgId)
|
||||
|
||||
@@ -15,11 +15,11 @@ namespace Bit.Core.Models.Domain
|
||||
public string AccessId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public SendType Type { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
public CipherString Notes { get; set; }
|
||||
public EncString Name { get; set; }
|
||||
public EncString Notes { get; set; }
|
||||
public SendFile File { get; set; }
|
||||
public SendText Text { get; set; }
|
||||
public CipherString Key { get; set; }
|
||||
public EncString Key { get; set; }
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public int AccessCount { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
@@ -27,6 +27,7 @@ namespace Bit.Core.Models.Domain
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool HideEmail { get; set; }
|
||||
|
||||
public Send() : base() { }
|
||||
|
||||
@@ -49,6 +50,7 @@ namespace Bit.Core.Models.Domain
|
||||
RevisionDate = data.RevisionDate;
|
||||
DeletionDate = data.DeletionDate;
|
||||
ExpirationDate = data.ExpirationDate;
|
||||
HideEmail = data.HideEmail;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Bit.Core.Models.Domain
|
||||
public string Id { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
public CipherString FileName { get; set; }
|
||||
public EncString FileName { get; set; }
|
||||
|
||||
public SendFile() : base() { }
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class SendText : Domain
|
||||
{
|
||||
public CipherString Text { get; set; }
|
||||
public EncString Text { get; set; }
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
public SendText() : base() { }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
@@ -30,22 +31,24 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
|
||||
Key = key;
|
||||
EncType = encType.Value;
|
||||
EncTypes.Add(encType.Value);
|
||||
|
||||
if (EncType == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
if (encType.Value == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key;
|
||||
MacKey = null;
|
||||
EncTypes.Add(EncryptionType.AesGcm256_B64);
|
||||
}
|
||||
else if (EncType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
else if (encType.Value == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = new ArraySegment<byte>(Key, 0, 16).ToArray();
|
||||
MacKey = new ArraySegment<byte>(Key, 16, 16).ToArray();
|
||||
}
|
||||
else if (EncType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
else if (encType.Value == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
{
|
||||
EncKey = new ArraySegment<byte>(Key, 0, 32).ToArray();
|
||||
MacKey = new ArraySegment<byte>(Key, 32, 32).ToArray();
|
||||
EncTypes.Add(EncryptionType.AesGcm256_B64);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -69,9 +72,9 @@ namespace Bit.Core.Models.Domain
|
||||
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; }
|
||||
public HashSet<EncryptionType> EncTypes { get; set; } = new HashSet<EncryptionType>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace Bit.Core.Models.Request
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string Key { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Request
|
||||
public SendFileApi File { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool HideEmail { get; set; }
|
||||
|
||||
public SendRequest(Send send, long? fileLength)
|
||||
{
|
||||
@@ -32,6 +33,7 @@ namespace Bit.Core.Models.Request
|
||||
Key = send.Key?.EncryptedString;
|
||||
Password = send.Password;
|
||||
Disabled = send.Disabled;
|
||||
HideEmail = send.HideEmail;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
|
||||
12
src/Core/Models/Response/AttachmentUploadDataReponse.cs
Normal file
12
src/Core/Models/Response/AttachmentUploadDataReponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
public class AttachmentUploadDataResponse
|
||||
{
|
||||
public string AttachmentId { get; set; }
|
||||
public FileUploadType FileUploadType { get; set; }
|
||||
public CipherResponse CipherResponse { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
12
src/Core/Models/Response/SendFileUploadDataResponse.cs
Normal file
12
src/Core/Models/Response/SendFileUploadDataResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
public class SendFileUploadDataResponse
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public FileUploadType FileUploadType { get; set; }
|
||||
public SendResponse SendResponse { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,6 @@ namespace Bit.Core.Models.Response
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool? HideEmail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Bit.Core.Models.View
|
||||
ExpirationDate = send.ExpirationDate;
|
||||
Disabled = send.Disabled;
|
||||
Password = send.Password;
|
||||
HideEmail = send.HideEmail;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
@@ -45,5 +46,6 @@ namespace Bit.Core.Models.View
|
||||
public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;
|
||||
public bool PendingDelete => DeletionDate <= DateTime.UtcNow;
|
||||
public string DisplayDate => DeletionDate.ToLocalTime().ToString("MMM d, yyyy, h:mm tt");
|
||||
public bool HideEmail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,9 +223,19 @@ namespace Bit.Core.Services
|
||||
public Task<SendResponse> PostSendAsync(SendRequest request) =>
|
||||
SendAsync<SendRequest, SendResponse>(HttpMethod.Post, "/sends", request, true, true);
|
||||
|
||||
public Task<SendFileUploadDataResponse> PostFileTypeSendAsync(SendRequest request) =>
|
||||
SendAsync<SendRequest, SendFileUploadDataResponse>(HttpMethod.Post, "/sends/file/v2", request, true, true);
|
||||
|
||||
public Task PostSendFileAsync(string sendId, string fileId, MultipartFormDataContent data) =>
|
||||
SendAsync<MultipartFormDataContent, object>(HttpMethod.Post, $"/sends/{sendId}/file/{fileId}", data, true, false);
|
||||
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
public Task<SendResponse> PostSendFileAsync(MultipartFormDataContent data) =>
|
||||
SendAsync<MultipartFormDataContent, SendResponse>(HttpMethod.Post, "/sends/file", data, true, true);
|
||||
|
||||
public Task<SendFileUploadDataResponse> RenewFileUploadUrlAsync(string sendId, string fileId) =>
|
||||
SendAsync<object, SendFileUploadDataResponse>(HttpMethod.Get, $"/sends/{sendId}/file/{fileId}", null, true, true);
|
||||
|
||||
public Task<SendResponse> PutSendAsync(string id, SendRequest request) =>
|
||||
SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true);
|
||||
|
||||
@@ -293,16 +303,26 @@ namespace Bit.Core.Services
|
||||
|
||||
#region Attachments APIs
|
||||
|
||||
public Task<CipherResponse> PostCipherAttachmentAsync(string id, MultipartFormDataContent data)
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
public Task<CipherResponse> PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data)
|
||||
{
|
||||
return SendAsync<MultipartFormDataContent, CipherResponse>(HttpMethod.Post,
|
||||
string.Concat("/ciphers/", id, "/attachment"), data, true, true);
|
||||
}
|
||||
|
||||
public Task<AttachmentUploadDataResponse> PostCipherAttachmentAsync(string id, AttachmentRequest request)
|
||||
{
|
||||
return SendAsync<AttachmentRequest, AttachmentUploadDataResponse>(HttpMethod.Post,
|
||||
$"/ciphers/{id}/attachment/v2", request, true, true);
|
||||
}
|
||||
|
||||
public Task<AttachmentResponse> GetAttachmentData(string cipherId, string attachmentId) =>
|
||||
SendAsync<AttachmentResponse>(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}", true);
|
||||
|
||||
public Task DeleteCipherAttachmentAsync(string id, string attachmentId)
|
||||
{
|
||||
return SendAsync<object, object>(HttpMethod.Delete,
|
||||
string.Concat("/ciphers/", id, "/attachment/", attachmentId), null, true, false);
|
||||
return SendAsync(HttpMethod.Delete,
|
||||
string.Concat("/ciphers/", id, "/attachment/", attachmentId), true);
|
||||
}
|
||||
|
||||
public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data,
|
||||
@@ -313,6 +333,13 @@ namespace Bit.Core.Services
|
||||
data, true, false);
|
||||
}
|
||||
|
||||
public Task<AttachmentUploadDataResponse> RenewAttachmentUploadUrlAsync(string cipherId, string attachmentId) =>
|
||||
SendAsync<AttachmentUploadDataResponse>(HttpMethod.Get, $"/ciphers/{cipherId}/attachment/{attachmentId}/renew", true);
|
||||
|
||||
public Task PostAttachmentFileAsync(string cipherId, string attachmentId, MultipartFormDataContent data) =>
|
||||
SendAsync(HttpMethod.Post,
|
||||
$"/ciphers/{cipherId}/attachment/{attachmentId}", data, true);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sync APIs
|
||||
@@ -427,6 +454,12 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendAsync(HttpMethod method, string path, bool authed) =>
|
||||
SendAsync<object, object>(method, path, null, authed, false);
|
||||
public Task SendAsync<TRequest>(HttpMethod method, string path, TRequest body, bool authed) =>
|
||||
SendAsync<TRequest, object>(method, path, body, authed, false);
|
||||
public Task<TResponse> SendAsync<TResponse>(HttpMethod method, string path, bool authed) =>
|
||||
SendAsync<object, TResponse>(method, path, null, authed, true);
|
||||
public async Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body,
|
||||
bool authed, bool hasResponse)
|
||||
{
|
||||
|
||||
197
src/Core/Services/AzureFileUploadService.cs
Normal file
197
src/Core/Services/AzureFileUploadService.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureFileUploadService : IAzureFileUploadService
|
||||
{
|
||||
private const long MAX_SINGLE_BLOB_UPLOAD_SIZE = 256 * 1024 * 1024; // 256 MiB
|
||||
private const int MAX_BLOCKS_PER_BLOB = 50000;
|
||||
private const decimal MAX_MOBILE_BLOCK_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
|
||||
public AzureFileUploadService()
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
NoCache = true,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task Upload(string uri, EncByteArray data, Func<Task<string>> renewalCallback)
|
||||
{
|
||||
if (data?.Buffer?.Length <= MAX_SINGLE_BLOB_UPLOAD_SIZE)
|
||||
{
|
||||
await AzureUploadBlob(uri, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AzureUploadBlocks(uri, data, renewalCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AzureUploadBlob(string uri, EncByteArray data)
|
||||
{
|
||||
using (var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
|
||||
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
|
||||
requestMessage.Headers.Add("x-ms-version", paramValues["sv"]);
|
||||
requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob");
|
||||
|
||||
requestMessage.Content = new ByteArrayContent(data.Buffer);
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Put;
|
||||
requestMessage.RequestUri = uriBuilder.Uri;
|
||||
|
||||
var blobResponse = await _httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (blobResponse.StatusCode != HttpStatusCode.Created)
|
||||
{
|
||||
throw new Exception("Failed to create Azure blob");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AzureUploadBlocks(string uri, EncByteArray data, Func<Task<string>> renewalFunc)
|
||||
{
|
||||
_httpClient.Timeout = TimeSpan.FromHours(3);
|
||||
var baseParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query);
|
||||
var blockSize = MaxBlockSize(baseParams["sv"]);
|
||||
var blockIndex = 0;
|
||||
var numBlocks = Math.Ceiling((decimal)data.Buffer.Length / blockSize);
|
||||
var blocksStaged = new List<string>();
|
||||
|
||||
if (numBlocks > MAX_BLOCKS_PER_BLOB)
|
||||
{
|
||||
throw new Exception($"Cannot upload file, exceeds maximum size of {blockSize * MAX_BLOCKS_PER_BLOB}");
|
||||
}
|
||||
|
||||
while (blockIndex < numBlocks)
|
||||
{
|
||||
uri = await RenewUriIfNecessary(uri, renewalFunc);
|
||||
var blockUriBuilder = new UriBuilder(uri);
|
||||
var blockId = EncodeBlockId(blockIndex);
|
||||
var blockParams = HttpUtility.ParseQueryString(blockUriBuilder.Query);
|
||||
blockParams.Add("comp", "block");
|
||||
blockParams.Add("blockid", blockId);
|
||||
blockUriBuilder.Query = blockParams.ToString();
|
||||
|
||||
using (var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
|
||||
requestMessage.Headers.Add("x-ms-version", baseParams["sv"]);
|
||||
requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob");
|
||||
|
||||
requestMessage.Content = new ByteArrayContent(data.Buffer.Skip(blockIndex * blockSize).Take(blockSize).ToArray());
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Put;
|
||||
requestMessage.RequestUri = blockUriBuilder.Uri;
|
||||
|
||||
var blockResponse = await _httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (blockResponse.StatusCode != HttpStatusCode.Created)
|
||||
{
|
||||
throw new Exception("Failed to create Azure block");
|
||||
}
|
||||
}
|
||||
|
||||
blocksStaged.Add(blockId);
|
||||
blockIndex++;
|
||||
}
|
||||
|
||||
using (var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
uri = await RenewUriIfNecessary(uri, renewalFunc);
|
||||
var blockListXml = GenerateBlockListXml(blocksStaged);
|
||||
var blockListUriBuilder = new UriBuilder(uri);
|
||||
var blockListParams = HttpUtility.ParseQueryString(blockListUriBuilder.Query);
|
||||
blockListParams.Add("comp", "blocklist");
|
||||
blockListUriBuilder.Query = blockListParams.ToString();
|
||||
|
||||
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
|
||||
requestMessage.Headers.Add("x-ms-version", baseParams["sv"]);
|
||||
|
||||
requestMessage.Content = new StringContent(blockListXml);
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Put;
|
||||
requestMessage.RequestUri = blockListUriBuilder.Uri;
|
||||
|
||||
var blockListResponse = await _httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (blockListResponse.StatusCode != HttpStatusCode.Created)
|
||||
{
|
||||
throw new Exception("Failed to PUT Azure block list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> RenewUriIfNecessary(string uri, Func<Task<string>> renewalFunc)
|
||||
{
|
||||
var uriParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query);
|
||||
|
||||
if (DateTime.TryParse(uriParams.Get("se") ?? "", out DateTime expiry) && expiry < DateTime.UtcNow.AddSeconds(1))
|
||||
{
|
||||
return await renewalFunc();
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GenerateBlockListXml(List<string> blocksStaged)
|
||||
{
|
||||
var xml = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?><BlockList>");
|
||||
foreach(var blockId in blocksStaged)
|
||||
{
|
||||
xml.Append($"<Latest>{blockId}</Latest>");
|
||||
}
|
||||
xml.Append("</BlockList>");
|
||||
return xml.ToString();
|
||||
}
|
||||
|
||||
private string EncodeBlockId(int index)
|
||||
{
|
||||
// Encoded blockId max size is 64, so pre-encoding max size is 48
|
||||
var paddedString = index.ToString("D48");
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(paddedString));
|
||||
}
|
||||
|
||||
private int MaxBlockSize(string version)
|
||||
{
|
||||
long maxSize = 4194304L; // 4 MiB
|
||||
if (CompareAzureVersions(version, "2019-12-12") >= 0)
|
||||
{
|
||||
maxSize = 4194304000L; // 4000 MiB
|
||||
}
|
||||
else if (CompareAzureVersions(version, "2016-05-31") >= 0)
|
||||
{
|
||||
maxSize = 104857600L; // 100 MiB
|
||||
}
|
||||
|
||||
return maxSize > MAX_MOBILE_BLOCK_SIZE ? (int)MAX_MOBILE_BLOCK_SIZE : (int) maxSize;
|
||||
}
|
||||
|
||||
private int CompareAzureVersions(string a, string b)
|
||||
{
|
||||
var v1Parts = a.Split('-').Select(p => int.Parse(p));
|
||||
var v2Parts = b.Split('-').Select(p => int.Parse(p));
|
||||
|
||||
return a[0] != b[0] ? a[0] - b[0] :
|
||||
a[1] != b[1] ? a[1] - b[1] :
|
||||
a[2] != b[2] ? a[2] - b[2] :
|
||||
0;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Core/Services/BitwardenFileUploadService.cs
Normal file
27
src/Core/Services/BitwardenFileUploadService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class BitwardenFileUploadService
|
||||
{
|
||||
public BitwardenFileUploadService(ApiService apiService)
|
||||
{
|
||||
_apiService = apiService;
|
||||
}
|
||||
|
||||
private readonly ApiService _apiService;
|
||||
|
||||
public async Task Upload(string encryptedFileName, EncByteArray encryptedFileData, Func<MultipartFormDataContent, Task> apiCall)
|
||||
{
|
||||
var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}")
|
||||
{
|
||||
{ new ByteArrayContent(encryptedFileData.Buffer), "data", encryptedFileName }
|
||||
};
|
||||
|
||||
await apiCall(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace Bit.Core.Services
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IFileUploadService _fileUploadService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly Func<ISearchService> _searchService;
|
||||
@@ -47,6 +48,7 @@ namespace Bit.Core.Services
|
||||
IUserService userService,
|
||||
ISettingsService settingsService,
|
||||
IApiService apiService,
|
||||
IFileUploadService fileUploadService,
|
||||
IStorageService storageService,
|
||||
II18nService i18nService,
|
||||
Func<ISearchService> searchService,
|
||||
@@ -57,6 +59,7 @@ namespace Bit.Core.Services
|
||||
_userService = userService;
|
||||
_settingsService = settingsService;
|
||||
_apiService = apiService;
|
||||
_fileUploadService = fileUploadService;
|
||||
_storageService = storageService;
|
||||
_i18nService = i18nService;
|
||||
_searchService = searchService;
|
||||
@@ -553,21 +556,47 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<Cipher> 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 orgKey = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId);
|
||||
var encFileName = await _cryptoService.EncryptAsync(filename, orgKey);
|
||||
var (attachmentKey, orgEncAttachmentKey) = await _cryptoService.MakeEncKeyAsync(orgKey);
|
||||
var encFileData = await _cryptoService.EncryptToBytesAsync(data, attachmentKey);
|
||||
|
||||
CipherResponse response;
|
||||
try
|
||||
{
|
||||
var request = new AttachmentRequest
|
||||
{
|
||||
Key = orgEncAttachmentKey.EncryptedString,
|
||||
FileName = encFileName.EncryptedString,
|
||||
FileSize = encFileData.Buffer.Length,
|
||||
};
|
||||
|
||||
var uploadDataResponse = await _apiService.PostCipherAttachmentAsync(cipher.Id, request);
|
||||
response = uploadDataResponse.CipherResponse;
|
||||
await _fileUploadService.UploadCipherAttachmentFileAsync(uploadDataResponse, encFileName.EncryptedString, encFileData);
|
||||
}
|
||||
catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound || e.Error.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
|
||||
{
|
||||
response = await LegacyServerAttachmentFileUploadAsync(cipher.Id, encFileName, encFileData, orgEncAttachmentKey);
|
||||
}
|
||||
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var cData = new CipherData(response, userId, cipher.CollectionIds);
|
||||
await UpsertAsync(cData);
|
||||
return new Cipher(cData);
|
||||
}
|
||||
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
private async Task<CipherResponse> LegacyServerAttachmentFileUploadAsync(string cipherId,
|
||||
EncString encFileName, EncByteArray encFileData, EncString key)
|
||||
{
|
||||
var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks);
|
||||
var fd = new MultipartFormDataContent(boundary);
|
||||
fd.Add(new StringContent(key.EncryptedString), "key");
|
||||
fd.Add(new StreamContent(new MemoryStream(encFileData.Buffer)), "data", encFileName.EncryptedString);
|
||||
return await _apiService.PostCipherAttachmentLegacyAsync(cipherId, fd);
|
||||
}
|
||||
|
||||
public async Task SaveCollectionsWithServerAsync(Cipher cipher)
|
||||
{
|
||||
var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList());
|
||||
@@ -706,11 +735,23 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
|
||||
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId)
|
||||
{
|
||||
string url;
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(new Uri(attachment.Url));
|
||||
var attachmentDownloadResponse = await _apiService.GetAttachmentData(cipherId, attachment.Id);
|
||||
url = attachmentDownloadResponse.Url;
|
||||
}
|
||||
// TODO: Delete this catch when all Servers are updated to respond to the above method
|
||||
catch (ApiException e) when (e.Error.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
url = attachment.Url;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(new Uri(url));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
@@ -787,7 +828,7 @@ namespace Bit.Core.Services
|
||||
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);
|
||||
fd.Add(new StreamContent(new MemoryStream(encData.Buffer)), "data", encFileName.EncryptedString);
|
||||
await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId);
|
||||
}
|
||||
|
||||
@@ -996,7 +1037,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
var modelPropInfo = modelType.GetProperty(propName);
|
||||
var modelProp = modelPropInfo.GetValue(model) as string;
|
||||
CipherString val = null;
|
||||
EncString val = null;
|
||||
if (!string.IsNullOrWhiteSpace(modelProp))
|
||||
{
|
||||
val = await _cryptoService.EncryptAsync(modelProp, key);
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
byte[] decEncKey = null;
|
||||
var encKeyCipher = new CipherString(encKey);
|
||||
var encKeyCipher = new EncString(encKey);
|
||||
if (encKeyCipher.EncryptionType == EncryptionType.AesCbc256_B64)
|
||||
{
|
||||
decEncKey = await DecryptToBytesAsync(encKeyCipher, key);
|
||||
@@ -205,7 +205,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return null;
|
||||
}
|
||||
_privateKey = await DecryptToBytesAsync(new CipherString(encPrivateKey), null);
|
||||
_privateKey = await DecryptToBytesAsync(new EncString(encPrivateKey), null);
|
||||
return _privateKey;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
public async Task<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt,
|
||||
KdfType kdf, int kdfIterations, CipherString protectedKeyCs = null)
|
||||
KdfType kdf, int kdfIterations, EncString protectedKeyCs = null)
|
||||
{
|
||||
if (protectedKeyCs == null)
|
||||
{
|
||||
@@ -398,27 +398,27 @@ namespace Bit.Core.Services
|
||||
{
|
||||
throw new Exception("No PIN protected key found.");
|
||||
}
|
||||
protectedKeyCs = new CipherString(pinProtectedKey);
|
||||
protectedKeyCs = new EncString(pinProtectedKey);
|
||||
}
|
||||
var pinKey = await MakePinKeyAysnc(pin, salt, kdf, kdfIterations);
|
||||
var decKey = await DecryptToBytesAsync(protectedKeyCs, pinKey);
|
||||
return new SymmetricCryptoKey(decKey);
|
||||
}
|
||||
|
||||
public async Task<Tuple<CipherString, SymmetricCryptoKey>> MakeShareKeyAsync()
|
||||
public async Task<Tuple<EncString, SymmetricCryptoKey>> MakeShareKeyAsync()
|
||||
{
|
||||
var shareKey = await _cryptoFunctionService.RandomBytesAsync(64);
|
||||
var publicKey = await GetPublicKeyAsync();
|
||||
var encShareKey = await RsaEncryptAsync(shareKey, publicKey);
|
||||
return new Tuple<CipherString, SymmetricCryptoKey>(encShareKey, new SymmetricCryptoKey(shareKey));
|
||||
return new Tuple<EncString, SymmetricCryptoKey>(encShareKey, new SymmetricCryptoKey(shareKey));
|
||||
}
|
||||
|
||||
public async Task<Tuple<string, CipherString>> MakeKeyPairAsync(SymmetricCryptoKey key = null)
|
||||
public async Task<Tuple<string, EncString>> 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<string, CipherString>(publicB64, privateEnc);
|
||||
return new Tuple<string, EncString>(publicB64, privateEnc);
|
||||
}
|
||||
|
||||
public async Task<SymmetricCryptoKey> MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations)
|
||||
@@ -447,20 +447,20 @@ namespace Bit.Core.Services
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
public async Task<Tuple<SymmetricCryptoKey, CipherString>> MakeEncKeyAsync(SymmetricCryptoKey key)
|
||||
public async Task<Tuple<SymmetricCryptoKey, EncString>> MakeEncKeyAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
var theKey = await GetKeyForEncryptionAsync(key);
|
||||
var encKey = await _cryptoFunctionService.RandomBytesAsync(64);
|
||||
return await BuildEncKeyAsync(theKey, encKey);
|
||||
}
|
||||
|
||||
public async Task<Tuple<SymmetricCryptoKey, CipherString>> RemakeEncKeyAsync(SymmetricCryptoKey key)
|
||||
public async Task<Tuple<SymmetricCryptoKey, EncString>> RemakeEncKeyAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
var encKey = await GetEncKeyAsync();
|
||||
return await BuildEncKeyAsync(key, encKey.Key);
|
||||
}
|
||||
|
||||
public async Task<CipherString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null)
|
||||
public async Task<EncString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if (plainValue == null)
|
||||
{
|
||||
@@ -469,7 +469,7 @@ namespace Bit.Core.Services
|
||||
return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key);
|
||||
}
|
||||
|
||||
public async Task<CipherString> EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null)
|
||||
public async Task<EncString> EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if (plainValue == null)
|
||||
{
|
||||
@@ -479,10 +479,10 @@ namespace Bit.Core.Services
|
||||
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);
|
||||
return new EncString(encObj.Type, data, iv, mac);
|
||||
}
|
||||
|
||||
public async Task<byte[]> EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null)
|
||||
public async Task<EncByteArray> EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null)
|
||||
{
|
||||
var encValue = await AesEncryptAsync(plainValue, key);
|
||||
var macLen = 0;
|
||||
@@ -491,17 +491,17 @@ namespace Bit.Core.Services
|
||||
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(new byte[] { (byte)encValue.Type }, 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;
|
||||
return new EncByteArray(encBytes);
|
||||
}
|
||||
|
||||
public async Task<CipherString> RsaEncryptAsync(byte[] data, byte[] publicKey = null)
|
||||
public async Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null)
|
||||
{
|
||||
if (publicKey == null)
|
||||
{
|
||||
@@ -512,21 +512,21 @@ namespace Bit.Core.Services
|
||||
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));
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Convert.ToBase64String(encBytes));
|
||||
}
|
||||
|
||||
public async Task<byte[]> DecryptToBytesAsync(CipherString cipherString, SymmetricCryptoKey key = null)
|
||||
public async Task<byte[]> DecryptToBytesAsync(EncString encString, 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);
|
||||
var iv = Convert.FromBase64String(encString.Iv);
|
||||
var data = Convert.FromBase64String(encString.Data);
|
||||
var mac = !string.IsNullOrWhiteSpace(encString.Mac) ? Convert.FromBase64String(encString.Mac) : null;
|
||||
return await AesDecryptToBytesAsync(encString.EncryptionType, data, iv, mac, key);
|
||||
}
|
||||
|
||||
public async Task<string> DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null)
|
||||
public async Task<string> DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null)
|
||||
{
|
||||
return await AesDecryptToUtf8Async(cipherString.EncryptionType, cipherString.Data,
|
||||
cipherString.Iv, cipherString.Mac, key);
|
||||
return await AesDecryptToUtf8Async(encString.EncryptionType, encString.Data,
|
||||
encString.Iv, encString.Mac, key);
|
||||
}
|
||||
|
||||
public async Task<byte[]> DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key)
|
||||
@@ -561,6 +561,14 @@ namespace Bit.Core.Services
|
||||
ivBytes = new ArraySegment<byte>(encBytes, 1, 16).ToArray();
|
||||
ctBytes = new ArraySegment<byte>(encBytes, 17, encBytes.Length - 17).ToArray();
|
||||
break;
|
||||
case EncryptionType.AesGcm256_B64:
|
||||
if (encBytes.Length < 13) // 1 + 12 + ctLength
|
||||
{
|
||||
return null;
|
||||
}
|
||||
ivBytes = new ArraySegment<byte>(encBytes, 1, 12).ToArray();
|
||||
ctBytes = new ArraySegment<byte>(encBytes, 13, encBytes.Length - 13).ToArray();
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -589,16 +597,27 @@ namespace Bit.Core.Services
|
||||
{
|
||||
var obj = new EncryptedObject
|
||||
{
|
||||
Key = await GetKeyForEncryptionAsync(key),
|
||||
Iv = await _cryptoFunctionService.RandomBytesAsync(16)
|
||||
Key = await GetKeyForEncryptionAsync(key)
|
||||
};
|
||||
obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey);
|
||||
if (obj.Key.MacKey != null)
|
||||
if (true)
|
||||
{
|
||||
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);
|
||||
obj.Type = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
obj.Iv = await _cryptoFunctionService.RandomBytesAsync(16);
|
||||
obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey, AesMode.CBC);
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: make this case live when we switch to GCM encryption
|
||||
obj.Type = EncryptionType.AesGcm256_B64;
|
||||
obj.Iv = await _cryptoFunctionService.RandomBytesAsync(12);
|
||||
obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey, AesMode.GCM);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
@@ -608,12 +627,15 @@ namespace Bit.Core.Services
|
||||
{
|
||||
var keyForEnc = await GetKeyForEncryptionAsync(key);
|
||||
var theKey = ResolveLegacyKey(encType, keyForEnc);
|
||||
if (theKey.MacKey != null && mac == null)
|
||||
|
||||
var macType = encType == EncryptionType.AesCbc128_HmacSha256_B64 ||
|
||||
encType == EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
if (macType && theKey.MacKey != null && mac == null)
|
||||
{
|
||||
// Mac required.
|
||||
return null;
|
||||
}
|
||||
if (theKey.EncType != encType)
|
||||
if (!theKey.EncTypes.Contains(encType))
|
||||
{
|
||||
// encType unavailable.
|
||||
return null;
|
||||
@@ -652,7 +674,8 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
var decBytes = await _cryptoFunctionService.AesDecryptAsync(dataBytes, ivBytes, encKey);
|
||||
var decBytes = await _cryptoFunctionService.AesDecryptAsync(dataBytes, ivBytes, encKey,
|
||||
encType == EncryptionType.AesGcm256_B64 ? AesMode.GCM : AesMode.CBC);
|
||||
return Encoding.UTF8.GetString(decBytes);
|
||||
}
|
||||
|
||||
@@ -662,12 +685,15 @@ namespace Bit.Core.Services
|
||||
|
||||
var keyForEnc = await GetKeyForEncryptionAsync(key);
|
||||
var theKey = ResolveLegacyKey(encType, keyForEnc);
|
||||
if (theKey.MacKey != null && mac == null)
|
||||
|
||||
var macType = encType == EncryptionType.AesCbc128_HmacSha256_B64 ||
|
||||
encType == EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
if (macType && theKey.MacKey != null && mac == null)
|
||||
{
|
||||
// Mac required.
|
||||
return null;
|
||||
}
|
||||
if (theKey.EncType != encType)
|
||||
if (!theKey.EncTypes.Contains(encType))
|
||||
{
|
||||
// encType unavailable.
|
||||
return null;
|
||||
@@ -694,7 +720,8 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey);
|
||||
return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey,
|
||||
encType == EncryptionType.AesGcm256_B64 ? AesMode.GCM : AesMode.CBC);
|
||||
}
|
||||
|
||||
private async Task<byte[]> RsaDecryptAsync(string encValue)
|
||||
@@ -763,7 +790,8 @@ namespace Bit.Core.Services
|
||||
|
||||
private SymmetricCryptoKey ResolveLegacyKey(EncryptionType encKey, SymmetricCryptoKey key)
|
||||
{
|
||||
if (encKey == EncryptionType.AesCbc128_HmacSha256_B64 && key.EncType == EncryptionType.AesCbc256_B64)
|
||||
if (encKey == EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.EncTypes.Contains(EncryptionType.AesCbc256_B64))
|
||||
{
|
||||
// Old encrypt-then-mac scheme, make a new key
|
||||
if (_legacyEtmKey == null)
|
||||
@@ -809,10 +837,10 @@ namespace Bit.Core.Services
|
||||
return phrase;
|
||||
}
|
||||
|
||||
private async Task<Tuple<SymmetricCryptoKey, CipherString>> BuildEncKeyAsync(SymmetricCryptoKey key,
|
||||
private async Task<Tuple<SymmetricCryptoKey, EncString>> BuildEncKeyAsync(SymmetricCryptoKey key,
|
||||
byte[] encKey)
|
||||
{
|
||||
CipherString encKeyEnc = null;
|
||||
EncString encKeyEnc = null;
|
||||
if (key.Key.Length == 32)
|
||||
{
|
||||
var newKey = await StretchKeyAsync(key);
|
||||
@@ -826,11 +854,12 @@ namespace Bit.Core.Services
|
||||
{
|
||||
throw new Exception("Invalid key size.");
|
||||
}
|
||||
return new Tuple<SymmetricCryptoKey, CipherString>(new SymmetricCryptoKey(encKey), encKeyEnc);
|
||||
return new Tuple<SymmetricCryptoKey, EncString>(new SymmetricCryptoKey(encKey), encKeyEnc);
|
||||
}
|
||||
|
||||
private class EncryptedObject
|
||||
{
|
||||
public EncryptionType Type { get; set; }
|
||||
public byte[] Iv { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
public byte[] Mac { get; set; }
|
||||
|
||||
80
src/Core/Services/FileUploadService.cs
Normal file
80
src/Core/Services/FileUploadService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Services {
|
||||
public class FileUploadService : IFileUploadService
|
||||
{
|
||||
public FileUploadService(ApiService apiService)
|
||||
{
|
||||
_apiService = apiService;
|
||||
_bitwardenFileUploadService = new BitwardenFileUploadService(apiService);
|
||||
_azureFileUploadService = new AzureFileUploadService();
|
||||
}
|
||||
|
||||
private readonly BitwardenFileUploadService _bitwardenFileUploadService;
|
||||
private readonly AzureFileUploadService _azureFileUploadService;
|
||||
private readonly ApiService _apiService;
|
||||
|
||||
public async Task UploadCipherAttachmentFileAsync(AttachmentUploadDataResponse uploadData,
|
||||
string encryptedFileName, EncByteArray encryptedFileData)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (uploadData.FileUploadType)
|
||||
{
|
||||
case FileUploadType.Direct:
|
||||
await _bitwardenFileUploadService.Upload(encryptedFileName, encryptedFileData,
|
||||
fd => _apiService.PostAttachmentFileAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId, fd));
|
||||
break;
|
||||
case FileUploadType.Azure:
|
||||
Func<Task<string>> renewalCallback = async () =>
|
||||
{
|
||||
var response = await _apiService.RenewAttachmentUploadUrlAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId);
|
||||
return response.Url;
|
||||
};
|
||||
await _azureFileUploadService.Upload(uploadData.Url, encryptedFileData, renewalCallback);
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Unkown file upload type: {uploadData.FileUploadType}");
|
||||
}
|
||||
} catch
|
||||
{
|
||||
await _apiService.DeleteCipherAttachmentAsync(uploadData.CipherResponse.Id, uploadData.AttachmentId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadSendFileAsync(SendFileUploadDataResponse uploadData, EncString fileName, EncByteArray encryptedFileData)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (uploadData.FileUploadType)
|
||||
{
|
||||
case FileUploadType.Direct:
|
||||
await _bitwardenFileUploadService.Upload(fileName.EncryptedString, encryptedFileData,
|
||||
fd => _apiService.PostSendFileAsync(uploadData.SendResponse.Id, uploadData.SendResponse.File.Id, fd));
|
||||
break;
|
||||
case FileUploadType.Azure:
|
||||
Func<Task<string>> renewalCallback = async () =>
|
||||
{
|
||||
var response = await _apiService.RenewFileUploadUrlAsync(uploadData.SendResponse.Id, uploadData.SendResponse.File.Id);
|
||||
return response.Url;
|
||||
};
|
||||
await _azureFileUploadService.Upload(uploadData.Url, encryptedFileData, renewalCallback);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown file upload type");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _apiService.DeleteSendAsync(uploadData.SendResponse.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,7 +633,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
var tasks = history.Select(async item =>
|
||||
{
|
||||
var decrypted = await _cryptoService.DecryptToUtf8Async(new CipherString(item.Password));
|
||||
var decrypted = await _cryptoService.DecryptToUtf8Async(new EncString(item.Password));
|
||||
return new GeneratedPasswordHistory
|
||||
{
|
||||
Password = decrypted,
|
||||
|
||||
@@ -143,16 +143,24 @@ namespace Bit.Core.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<byte[]> AesEncryptAsync(byte[] data, byte[] iv, byte[] key)
|
||||
public Task<byte[]> AesEncryptAsync(byte[] data, byte[] iv, byte[] key, AesMode mode)
|
||||
{
|
||||
var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
if (mode == AesMode.GCM)
|
||||
{
|
||||
return Task.FromResult(_cryptoPrimitiveService.AesGcmEncrypt(data, iv, key));
|
||||
}
|
||||
var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(AesModeToSymmetricAlgorithm(mode));
|
||||
var cryptoKey = provider.CreateSymmetricKey(key);
|
||||
return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data, iv));
|
||||
}
|
||||
|
||||
public Task<byte[]> AesDecryptAsync(byte[] data, byte[] iv, byte[] key)
|
||||
public Task<byte[]> AesDecryptAsync(byte[] data, byte[] iv, byte[] key, AesMode mode)
|
||||
{
|
||||
var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
if (mode == AesMode.GCM)
|
||||
{
|
||||
return Task.FromResult(_cryptoPrimitiveService.AesGcmDecrypt(data, iv, key));
|
||||
}
|
||||
var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(AesModeToSymmetricAlgorithm(mode));
|
||||
var cryptoKey = provider.CreateSymmetricKey(key);
|
||||
return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data, iv));
|
||||
}
|
||||
@@ -286,5 +294,18 @@ namespace Bit.Core.Services
|
||||
throw new ArgumentException($"Invalid hkdf algorithm type, {hkdfAlgorithm}");
|
||||
}
|
||||
}
|
||||
|
||||
private SymmetricAlgorithm AesModeToSymmetricAlgorithm(AesMode aesMode)
|
||||
{
|
||||
switch (aesMode)
|
||||
{
|
||||
case AesMode.CBC:
|
||||
return SymmetricAlgorithm.AesCbcPkcs7;
|
||||
case AesMode.GCM:
|
||||
return SymmetricAlgorithm.AesGcm;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid aes mode type, {aesMode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
@@ -25,11 +26,13 @@ namespace Bit.Core.Services
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private Task<List<SendView>> _getAllDecryptedTask;
|
||||
private readonly IFileUploadService _fileUploadService;
|
||||
|
||||
public SendService(
|
||||
ICryptoService cryptoService,
|
||||
IUserService userService,
|
||||
IApiService apiService,
|
||||
IFileUploadService fileUploadService,
|
||||
IStorageService storageService,
|
||||
II18nService i18nService,
|
||||
ICryptoFunctionService cryptoFunctionService)
|
||||
@@ -37,6 +40,7 @@ namespace Bit.Core.Services
|
||||
_cryptoService = cryptoService;
|
||||
_userService = userService;
|
||||
_apiService = apiService;
|
||||
_fileUploadService = fileUploadService;
|
||||
_storageService = storageService;
|
||||
_i18nService = i18nService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
@@ -77,7 +81,7 @@ namespace Bit.Core.Services
|
||||
await DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
|
||||
public async Task<(Send send, EncByteArray encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
|
||||
string password, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if (model.Key == null)
|
||||
@@ -97,8 +101,9 @@ namespace Bit.Core.Services
|
||||
Key = await _cryptoService.EncryptAsync(model.Key, key),
|
||||
Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
|
||||
Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
|
||||
HideEmail = model.HideEmail
|
||||
};
|
||||
byte[] encryptedFileData = null;
|
||||
EncByteArray encryptedFileData = null;
|
||||
|
||||
if (password != null)
|
||||
{
|
||||
@@ -192,10 +197,10 @@ namespace Bit.Core.Services
|
||||
_decryptedSendsCache = null;
|
||||
}
|
||||
|
||||
public async Task<string> SaveWithServerAsync(Send send, byte[] encryptedFileData)
|
||||
public async Task<string> SaveWithServerAsync(Send send, EncByteArray encryptedFileData)
|
||||
{
|
||||
var request = new SendRequest(send, encryptedFileData?.LongLength);
|
||||
SendResponse response;
|
||||
var request = new SendRequest(send, encryptedFileData?.Buffer?.LongLength);
|
||||
SendResponse response = default;
|
||||
if (send.Id == null)
|
||||
{
|
||||
switch (send.Type)
|
||||
@@ -204,13 +209,23 @@ namespace Bit.Core.Services
|
||||
response = await _apiService.PostSendAsync(request);
|
||||
break;
|
||||
case SendType.File:
|
||||
var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}")
|
||||
{
|
||||
{ new StringContent(JsonConvert.SerializeObject(request)), "model" },
|
||||
{ new ByteArrayContent(encryptedFileData), "data", send.File.FileName.EncryptedString }
|
||||
};
|
||||
try{
|
||||
var uploadDataResponse = await _apiService.PostFileTypeSendAsync(request);
|
||||
response = uploadDataResponse.SendResponse;
|
||||
|
||||
response = await _apiService.PostSendFileAsync(fd);
|
||||
await _fileUploadService.UploadSendFileAsync(uploadDataResponse, send.File.FileName, encryptedFileData);
|
||||
}
|
||||
catch (ApiException e) when (e.Error.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
response = await LegacyServerSendFileUpload(request, send, encryptedFileData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (response != default){
|
||||
await _apiService.DeleteSendAsync(response.Id);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Cannot save unknown Send type {send.Type}");
|
||||
@@ -227,6 +242,17 @@ namespace Bit.Core.Services
|
||||
return response.Id;
|
||||
}
|
||||
|
||||
[Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")]
|
||||
private async Task<SendResponse> LegacyServerSendFileUpload(SendRequest request, Send send, EncByteArray encryptedFileData) {
|
||||
var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}")
|
||||
{
|
||||
{ new StringContent(JsonConvert.SerializeObject(request)), "model" },
|
||||
{ new ByteArrayContent(encryptedFileData.Buffer), "data", send.File.FileName.EncryptedString }
|
||||
};
|
||||
|
||||
return await _apiService.PostSendFileAsync(fd);
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(params SendData[] sends)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
|
||||
@@ -327,6 +327,7 @@ namespace Bit.Core.Services
|
||||
await _userService.SetSecurityStampAsync(response.SecurityStamp);
|
||||
var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o));
|
||||
await _userService.ReplaceOrganizationsAsync(organizations);
|
||||
await _userService.SetEmailVerifiedAsync(response.EmailVerified);
|
||||
}
|
||||
|
||||
private async Task SyncFoldersAsync(string userId, List<FolderResponse> response)
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Bit.Core.Services
|
||||
private string _stamp;
|
||||
private KdfType? _kdf;
|
||||
private int? _kdfIterations;
|
||||
private bool? _emailVerified;
|
||||
|
||||
private const string Keys_UserId = "userId";
|
||||
private const string Keys_UserEmail = "userEmail";
|
||||
@@ -22,6 +23,7 @@ namespace Bit.Core.Services
|
||||
private const string Keys_Kdf = "kdf";
|
||||
private const string Keys_KdfIterations = "kdfIterations";
|
||||
private const string Keys_OrganizationsFormat = "organizations_{0}";
|
||||
private const string Keys_EmailVerified = "emailVerified";
|
||||
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ITokenService _tokenService;
|
||||
@@ -51,6 +53,12 @@ namespace Bit.Core.Services
|
||||
await _storageService.SaveAsync(Keys_Stamp, stamp);
|
||||
}
|
||||
|
||||
public async Task SetEmailVerifiedAsync(bool emailVerified)
|
||||
{
|
||||
_emailVerified = emailVerified;
|
||||
await _storageService.SaveAsync(Keys_EmailVerified, emailVerified);
|
||||
}
|
||||
|
||||
public async Task<string> GetUserIdAsync()
|
||||
{
|
||||
if (_userId == null)
|
||||
@@ -78,6 +86,15 @@ namespace Bit.Core.Services
|
||||
return _stamp;
|
||||
}
|
||||
|
||||
public async Task<bool> GetEmailVerifiedAsync()
|
||||
{
|
||||
if (_emailVerified == null)
|
||||
{
|
||||
_emailVerified = await _storageService.GetAsync<bool>(Keys_EmailVerified);
|
||||
}
|
||||
return _emailVerified.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public async Task<KdfType?> GetKdfAsync()
|
||||
{
|
||||
if (_kdf == null)
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Bit.Core.Services
|
||||
_loggedOutCallback = loggedOutCallback;
|
||||
}
|
||||
|
||||
public CipherString PinProtectedKey { get; set; } = null;
|
||||
public EncString PinProtectedKey { get; set; } = null;
|
||||
public bool BiometricLocked { get; set; } = true;
|
||||
|
||||
public async Task<bool> IsLockedAsync()
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Bit.Core.Utilities
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Uri GetUri(string uriString)
|
||||
public static Uri GetUri(string uriString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uriString))
|
||||
{
|
||||
|
||||
@@ -40,13 +40,14 @@ namespace Bit.Core.Utilities
|
||||
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,
|
||||
var fileUploadService = new FileUploadService(apiService);
|
||||
var cipherService = new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService,
|
||||
storageService, i18nService, () => searchService, clearCipherCacheKey, allClearCipherCacheKeys);
|
||||
var folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
||||
i18nService, cipherService);
|
||||
var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||
var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
|
||||
cryptoFunctionService);
|
||||
var sendService = new SendService(cryptoService, userService, apiService, fileUploadService, storageService,
|
||||
i18nService, cryptoFunctionService);
|
||||
searchService = new SearchService(cipherService, sendService);
|
||||
var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService,
|
||||
storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden.autofill</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.1</string>
|
||||
<string>2.10.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
_vaultTimeoutService.PinProtectedKey);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var protectedPin = await _storageService.GetAsync<string>(Bit.Core.Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
failed = decPin != inputtedValue;
|
||||
if (!failed)
|
||||
{
|
||||
@@ -191,7 +191,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Bit.Core.Constants.ProtectedPin);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key2);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
_vaultTimeoutService.PinProtectedKey = await _cryptoService.EncryptAsync(key2.Key, pinKey);
|
||||
|
||||
@@ -39,6 +39,17 @@ namespace Bit.iOS.Core.Services
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
public byte[] AesGcmEncrypt(byte[] data, byte[] iv, byte[] key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
public byte[] AesGcmDecrypt(byte[] data, byte[] iv, byte[] key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -437,6 +437,11 @@ namespace Bit.iOS.Core.Services
|
||||
return iOSHelpers.GetSystemUpTimeMilliseconds() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
public void CloseMainApp()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
|
||||
{
|
||||
if (sender is UIImagePickerController picker)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.1</string>
|
||||
<string>2.10.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.1</string>
|
||||
<string>2.10.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleIconName</key>
|
||||
|
||||
@@ -144,13 +144,27 @@
|
||||
<ImageAsset Include="Resources\Assets.xcassets\AppIcons.appiconset\Contents.json">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\Contents.json" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo%402x.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo%403x.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%402x.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%403x.png" />
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\Contents.json">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo%402x.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo%403x.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%402x.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%403x.png">
|
||||
<Visible>false</Visible>
|
||||
</ImageAsset>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InterfaceDefinition Include="LaunchScreen.storyboard" />
|
||||
|
||||
62
test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs
Normal file
62
test/Core.Test/AutoFixture/Cipher/CipherCustomizations.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using AutoFixture;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture
|
||||
|
||||
{
|
||||
internal class OrganizationCipher : ICustomization
|
||||
{
|
||||
public string OrganizationId { get; set; }
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.With(c => c.OrganizationId, OrganizationId ?? Guid.NewGuid().ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
internal class UserCipher : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.Without(c => c.OrganizationId));
|
||||
}
|
||||
}
|
||||
|
||||
internal class UserCipherAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public UserCipherAutoDataAttribute() : base(new SutProviderCustomization(),
|
||||
new UserCipher())
|
||||
{ }
|
||||
}
|
||||
internal class InlineUserCipherAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineUserCipherAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(UserCipher) }, values)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlineKnownUserCipherAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineKnownUserCipherAutoDataAttribute(string userId, params object[] values) : base(new ICustomization[]
|
||||
{ new SutProviderCustomization(), new UserCipher() }, values)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class OrganizationCipherAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public OrganizationCipherAutoDataAttribute(string organizationId = null) : base(new SutProviderCustomization(),
|
||||
new OrganizationCipher { OrganizationId = organizationId ?? null })
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlineOrganizationCipherAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineOrganizationCipherAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(OrganizationCipher) }, values)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,17 @@ namespace Bit.Core.Test.Models.Domain
|
||||
var prefix = "decrypted_";
|
||||
var prefixBytes = Encoding.UTF8.GetBytes(prefix);
|
||||
|
||||
cryptoService.DecryptToBytesAsync(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => prefixBytes.Concat(Encoding.UTF8.GetBytes(((CipherString)info[0]).EncryptedString)).ToArray());
|
||||
cryptoService.DecryptToBytesAsync(Arg.Any<EncString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => prefixBytes.Concat(Encoding.UTF8.GetBytes(((EncString)info[0]).EncryptedString)).ToArray());
|
||||
cryptoService.DecryptFromBytesAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => prefixBytes.Concat((byte[])info[0]).ToArray());
|
||||
cryptoService.DecryptToUtf8Async(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => $"{prefix}{((CipherString)info[0]).EncryptedString}");
|
||||
cryptoService.DecryptToUtf8Async(Arg.Any<EncString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => $"{prefix}{((EncString)info[0]).EncryptedString}");
|
||||
ServiceContainer.Register("cryptoService", cryptoService);
|
||||
|
||||
var view = await send.DecryptAsync();
|
||||
|
||||
string expectedDecryptionString(CipherString encryptedString) =>
|
||||
string expectedDecryptionString(EncString encryptedString) =>
|
||||
encryptedString?.EncryptedString == null ? null : $"{prefix}{encryptedString.EncryptedString}";
|
||||
|
||||
TestHelper.AssertPropertyEqual(send, view, "Name", "Notes", "File", "Text", "Key", "UserId");
|
||||
|
||||
96
test/Core.Test/Services/CipherServiceTests.cs
Normal file
96
test/Core.Test/Services/CipherServiceTests.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
public class CipherServiceTests
|
||||
{
|
||||
[Theory, UserCipherAutoData]
|
||||
public async Task SaveWithServerAsync_PrefersFileUploadService(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher, string fileName, EncByteArray data, AttachmentUploadDataResponse uploadDataResponse, EncString encKey)
|
||||
{
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(new EncString(fileName));
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptToBytesAsync(data.Buffer, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(data);
|
||||
sutProvider.GetDependency<ICryptoService>().MakeEncKeyAsync(Arg.Any<SymmetricCryptoKey>()).Returns(new Tuple<SymmetricCryptoKey, EncString>(null, encKey));
|
||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentAsync(cipher.Id, Arg.Any<AttachmentRequest>())
|
||||
.Returns(uploadDataResponse);
|
||||
|
||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer);
|
||||
|
||||
await sutProvider.GetDependency<IFileUploadService>().Received(1)
|
||||
.UploadCipherAttachmentFileAsync(uploadDataResponse, fileName, data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineUserCipherAutoData(HttpStatusCode.NotFound)]
|
||||
[InlineUserCipherAutoData(HttpStatusCode.MethodNotAllowed)]
|
||||
public async Task SaveWithServerAsync_FallsBackToLegacyFormData(HttpStatusCode statusCode,
|
||||
SutProvider<CipherService> sutProvider, Cipher cipher, string fileName, EncByteArray data,
|
||||
CipherResponse response, EncString encKey)
|
||||
{
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(new EncString(fileName));
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptToBytesAsync(data.Buffer, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(data);
|
||||
sutProvider.GetDependency<ICryptoService>().MakeEncKeyAsync(Arg.Any<SymmetricCryptoKey>()).Returns(new Tuple<SymmetricCryptoKey, EncString>(null, encKey));
|
||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentAsync(cipher.Id, Arg.Any<AttachmentRequest>())
|
||||
.Throws(new ApiException(new ErrorResponse {StatusCode = statusCode}));
|
||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>())
|
||||
.Returns(response);
|
||||
|
||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer);
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||
.PostCipherAttachmentLegacyAsync(cipher.Id, Arg.Any<MultipartFormDataContent>());
|
||||
}
|
||||
|
||||
[Theory, UserCipherAutoData]
|
||||
public async Task SaveWithServerAsync_ThrowsOnBadRequestApiException(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher, string fileName, EncByteArray data, EncString encKey)
|
||||
{
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(fileName, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(new EncString(fileName));
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptToBytesAsync(data.Buffer, Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(data);
|
||||
sutProvider.GetDependency<ICryptoService>().MakeEncKeyAsync(Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(new Tuple<SymmetricCryptoKey, EncString>(null, encKey));
|
||||
var expectedException = new ApiException(new ErrorResponse { StatusCode = HttpStatusCode.BadRequest });
|
||||
sutProvider.GetDependency<IApiService>().PostCipherAttachmentAsync(cipher.Id, Arg.Any<AttachmentRequest>())
|
||||
.Throws(expectedException);
|
||||
|
||||
var actualException = await Assert.ThrowsAsync<ApiException>(async () =>
|
||||
await sutProvider.Sut.SaveAttachmentRawWithServerAsync(cipher, fileName, data.Buffer));
|
||||
|
||||
Assert.Equal(expectedException.Error.StatusCode, actualException.Error.StatusCode);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization), typeof(SymmetricCryptoKeyCustomization))]
|
||||
public async Task DownloadAndDecryptAttachmentAsync_RequestsTimeLimitedUrl(SutProvider<CipherService> sutProvider,
|
||||
string cipherId, AttachmentView attachment, AttachmentResponse response)
|
||||
{
|
||||
sutProvider.GetDependency<IApiService>().GetAttachmentData(cipherId, attachment.Id)
|
||||
.Returns(response);
|
||||
|
||||
await sutProvider.Sut.DownloadAndDecryptAttachmentAsync(cipherId, attachment, null);
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1).GetAttachmentData(cipherId, attachment.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ using Bit.Core.Models.Request;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using System.Linq.Expressions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Exceptions;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
@@ -172,16 +174,15 @@ namespace Bit.Core.Test.Services
|
||||
send.Id = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
|
||||
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
|
||||
|
||||
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
|
||||
var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content"));
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
|
||||
|
||||
Predicate<SendRequest> sendRequestPredicate = r =>
|
||||
{
|
||||
// Note Send -> SendRequest tested in SendRequestTests
|
||||
TestHelper.AssertPropertyEqual(new SendRequest(send, fileContentBytes?.LongLength), r);
|
||||
TestHelper.AssertPropertyEqual(new SendRequest(send, fileContentBytes.Buffer?.LongLength), r);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -200,35 +201,21 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task SaveWithServerAsync_NewFileSend_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
|
||||
public async Task SaveWithServerAsync_NewFileSend_AzureUpload_Success(SutProvider<SendService> sutProvider, string userId, SendFileUploadDataResponse response, Send send)
|
||||
{
|
||||
send.Id = null;
|
||||
response.FileUploadType = FileUploadType.Azure;
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
|
||||
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
|
||||
sutProvider.GetDependency<IApiService>().PostFileTypeSendAsync(Arg.Any<SendRequest>()).Returns(response);
|
||||
|
||||
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
|
||||
var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content"));
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
|
||||
|
||||
Predicate<MultipartFormDataContent> formDataPredicate = fd =>
|
||||
{
|
||||
Assert.Equal(2, fd.Count()); // expect a request and file content
|
||||
|
||||
var expectedRequest = JsonConvert.SerializeObject(new SendRequest(send, fileContentBytes?.LongLength));
|
||||
var actualRequest = fd.First().ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
Assert.Equal(expectedRequest, actualRequest);
|
||||
|
||||
var actualFileContent = fd.Skip(1).First().ReadAsByteArrayAsync().GetAwaiter().GetResult();
|
||||
Assert.Equal(fileContentBytes, actualFileContent);
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.File:
|
||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||
.PostSendFileAsync(Arg.Is<MultipartFormDataContent>(f => formDataPredicate(f)));
|
||||
await sutProvider.GetDependency<IFileUploadService>().Received(1).UploadSendFileAsync(response, send.File.FileName, fileContentBytes);
|
||||
break;
|
||||
case SendType.Text:
|
||||
default:
|
||||
@@ -236,6 +223,23 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task SaveWithServerAsync_NewFileSend_LegacyFallback_Success(SutProvider<SendService> sutProvider, string userId, Send send, SendResponse response)
|
||||
{
|
||||
send.Id = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
var error = new ErrorResponse(null, System.Net.HttpStatusCode.NotFound);
|
||||
sutProvider.GetDependency<IApiService>().PostFileTypeSendAsync(Arg.Any<SendRequest>()).Throws(new ApiException(error));
|
||||
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
|
||||
|
||||
var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content"));
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1).PostSendFileAsync(Arg.Any<MultipartFormDataContent>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
@@ -335,12 +339,12 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
byte[] getPbkdf(string password, byte[] key) =>
|
||||
prefixBytes.Concat(Encoding.UTF8.GetBytes(password)).Concat(key).ToArray();
|
||||
CipherString encryptBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
|
||||
CipherString encrypt(string secret, SymmetricCryptoKey key) =>
|
||||
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
|
||||
byte[] encryptFileBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||
secret.Concat(key.Key).ToArray();
|
||||
EncString encryptBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||
new EncString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
|
||||
EncString encrypt(string secret, SymmetricCryptoKey key) =>
|
||||
new EncString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
|
||||
EncByteArray encryptFileBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||
new EncByteArray(secret.Concat(key.Key).ToArray());
|
||||
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
|
||||
.Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
|
||||
@@ -370,7 +374,7 @@ namespace Bit.Core.Test.Services
|
||||
case SendType.File:
|
||||
// Only set filename
|
||||
TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName);
|
||||
Assert.Equal(encryptFileBytes(fileData, view.CryptoKey), encryptedFileData);
|
||||
Assert.Equal(encryptFileBytes(fileData, view.CryptoKey).Buffer, encryptedFileData.Buffer);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Untested send type");
|
||||
|
||||
Reference in New Issue
Block a user