1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-26 05:03:39 +00:00

Compare commits

..

21 Commits

Author SHA1 Message Date
Kyle Spearrin
02ad4adff5 support for aes gcm crypto 2021-05-05 15:02:19 -04:00
Matt Portune
faa6904ce3 update firebase messaging lib and implementation (#1383)
* update firebase messaging lib and implementation

* update clean-fdroid script for updated firebase
2021-05-03 13:36:09 -04:00
Matt Portune
c27da8e7c4 fix for disappearing share sheet (#1386) 2021-04-27 17:49:01 -04:00
Matt Gibson
3ef5ca9cc0 Limit file upload sizes to 100MB (#1385)
both iOS and Android are having trouble with the current method
of loading the entire file to memory, encrypting it, and sending
to azure in one go.

We will need to come up with a chunking scheme to support
larger files in the future
2021-04-27 15:14:54 -05:00
Vincent Salucci
2fbd3b4538 [Version] Bump to 2.10.0 (#1382) 2021-04-22 12:00:01 -05:00
laymanZ
b3b21ea6b1 add microsoft package to support beta/dev/canary (#1370)
* add microsoft package to support beta/dev/canary

* Add package name in the necessary file
2021-04-22 00:16:19 -04:00
Matt Gibson
a3b4ede8f3 Use CipherByteArray to signify encrypted byte[] (#1366)
* Use CipherByteArray to signify encrypted  byte[]

* Rename CipherString and CipherByteArray to EncString and EncByteArray
2021-04-21 15:27:14 -05:00
Matt Portune
10ea6a86e3 clear cache on logout (#1375) 2021-04-19 15:38:49 -04:00
Thomas Rittson
3b2b37b3b0 Use UserService to manage emailVerified (#1367) 2021-04-15 14:54:58 +10:00
Matt Gibson
75e27ffbe3 Move renew endpoint to fix overlapping endpoint issue (#1362) 2021-04-12 09:45:17 -05:00
Thomas Rittson
a2cff6da28 Require user to verify email to use file Send (#1360) 2021-04-08 06:42:00 +10:00
Daniel James Smith
fe80fd0ba1 Replaced appveyor build badge with one from Github Workflow (#1350)
* Deleted appveyor.yml

* Remove Include of appveyor.yml from sln-file

* Deleted ci-build-apks.ps1 referenced by appveyor.yml

* Replaced build badge in README.md
2021-04-05 07:28:37 -07:00
dldhk97
5c6b9fa471 Add support for Soul browser (#1348) 2021-04-01 14:01:21 -04:00
Matt Portune
d926565358 Share-to-Send for Android (#1343)
* Android implementation

* remove iOS attempt for now
2021-03-31 10:19:05 -04:00
Matt Gibson
ce0b8bc62d Attachment azure upload blobs (#1345)
* Update Size limits

* Add new Api paths for direct upload of Cipher Attachments

* Add Attachment upload to fileUploadService

* Save with direct upload and fallback to legacy uplaod

CipherID is required for direct upload to request an upload URL

* Inform on when to remove legacy code

* Test Attachment upload
2021-03-30 18:42:43 -05:00
Thomas Rittson
04aeddc5de Hide email address in Sends (#1340)
* Add HideEmail model properties and locale strings

* Fix UI strings

* Add HideEmail to SendService

* Add HideEmail option to UI

* Tidy up declarations

* Add Bitwarden Send translation warning
2021-03-29 12:01:42 -04:00
Matt Gibson
13ffbe911a Send azure upload (#1334)
* Add direct upload api endpoints

* Create azure upload service

* Update max file size

* Update send file upload test

* Move internationalization string to correct document

* Allow for one shot blob uploads

* Remove unused helper

* Use FileUploadService

Fallback to legacy method on old server implementations.
2021-03-29 09:45:04 -05:00
Jamal
ab04759b0e Add support for Styx browser. (#1336)
https://github.com/jamal2362/Styx
2021-03-23 15:09:32 -04:00
Stéphane Lenclud
798cfef391 Add support for Fulguris browser. (#1333) 2021-03-22 16:54:55 -04:00
Matt Portune
3b5cdfe03c bump to 2.9.2 (#1321) 2021-03-16 15:56:28 -04:00
Contribucious
63449a3832 [KnownUsernameField] Entries update (Amazon) (#1320) 2021-03-16 14:57:45 -04:00
94 changed files with 1377 additions and 618 deletions

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/mobile?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/mobile)
[![Github Workflow build on master](https://github.com/bitwarden/mobile/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/bitwarden/mobile/actions/workflows/build.yml?query=branch:master)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-mobile/localized.svg)](https://crowdin.com/project/bitwarden-mobile)
[![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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" />

View File

@@ -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",

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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

View File

@@ -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)

View File

@@ -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"/>

View File

@@ -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;
}
}
}

View File

@@ -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)
{

View File

@@ -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 "########################################"

View File

@@ -44,5 +44,6 @@ namespace Bit.App.Abstractions
void OpenAutofillSettings();
bool UsingDarkTheme();
long GetActiveTime();
void CloseMainApp();
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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));
}

View File

@@ -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()
{

View File

@@ -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)
{

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum AesMode : byte
{
CBC = 0,
GCM = 1,
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace Bit.Core.Enums
{
public enum FileUploadType
{
Direct = 0,
Azure = 1,
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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);
}

View File

@@ -0,0 +1,12 @@
namespace Bit.Core.Models.Domain
{
public class EncByteArray
{
public byte[] Buffer { get; }
public EncByteArray(byte[] encryptedByteArray)
{
Buffer = encryptedByteArray;
}
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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() { }

View File

@@ -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() { }

View File

@@ -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>();
}
}

View File

@@ -6,5 +6,6 @@ namespace Bit.Core.Models.Request
{
public string FileName { get; set; }
public string Key { get; set; }
public long FileSize { get; set; }
}
}

View File

@@ -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)
{

View 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; }
}
}

View 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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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)
{

View 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;
}
}
}

View 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);
}
}
}

View File

@@ -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);

View File

@@ -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; }

View 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;
}
}
}
}

View File

@@ -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,

View File

@@ -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}");
}
}
}
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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))
{

View File

@@ -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,

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View 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)
{ }
}
}

View File

@@ -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");

View 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);
}
}
}

View File

@@ -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");