diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index 0f6e643b9c..d5aaef9d89 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -9,14 +9,6 @@
..\..\docker\Docker.dcproj
-
-
-
-
-
-
-
-
diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs
index 210a9e254f..892e969e8d 100644
--- a/src/Api/Controllers/AccountsController.cs
+++ b/src/Api/Controllers/AccountsController.cs
@@ -12,6 +12,9 @@ using System.Linq;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Core;
+using System.IO;
+using Newtonsoft.Json;
+using Bit.Core.Models.Business;
namespace Bit.Api.Controllers
{
@@ -378,7 +381,7 @@ namespace Bit.Api.Controllers
}
[HttpPost("premium")]
- public async Task PostPremium([FromBody]PremiumRequestModel model)
+ public async Task PostPremium(PremiumRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if(user == null)
@@ -386,7 +389,32 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException();
}
- await _userService.SignUpPremiumAsync(user, model.PaymentToken, model.AdditionalStorageGb.GetValueOrDefault(0));
+ var valid = model.Validate(_globalSettings);
+ UserLicense license = null;
+ if(valid && model.License != null)
+ {
+ try
+ {
+ using (var stream = model.License.OpenReadStream())
+ using(var reader = new StreamReader(stream))
+ {
+ var s = await reader.ReadToEndAsync();
+ license = JsonConvert.DeserializeObject(s);
+ }
+ }
+ catch
+ {
+ valid = false;
+ }
+ }
+
+ if(!valid)
+ {
+ throw new BadRequestException("Invalid license.");
+ }
+
+ await _userService.SignUpPremiumAsync(user, model.PaymentToken,
+ model.AdditionalStorageGb.GetValueOrDefault(0), license);
return new ProfileResponseModel(user, null);
}
diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj
index b68cd4f788..e2d119fba0 100644
--- a/src/Billing/Billing.csproj
+++ b/src/Billing/Billing.csproj
@@ -8,10 +8,6 @@
bitwarden-Billing
-
-
-
-
diff --git a/src/Billing/licensing.cer b/src/Billing/licensing.cer
deleted file mode 100644
index 0dbb09c3c6..0000000000
Binary files a/src/Billing/licensing.cer and /dev/null differ
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index bae6225622..d466deaff6 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs
index 730f0acca7..99dd3ec1e6 100644
--- a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs
+++ b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs
@@ -1,10 +1,28 @@
-using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http;
+using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
namespace Bit.Core.Models.Api
{
- public class PremiumRequestModel : PaymentRequestModel
+ public class PremiumRequestModel : IValidatableObject
{
+ public string PaymentToken { get; set; }
[Range(0, 99)]
public short? AdditionalStorageGb { get; set; }
+ public IFormFile License { get; set; }
+
+ public bool Validate(GlobalSettings globalSettings)
+ {
+ return (License == null && !globalSettings.SelfHosted) ||
+ (License != null && globalSettings.SelfHosted);
+ }
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if(string.IsNullOrWhiteSpace(PaymentToken) && License == null)
+ {
+ yield return new ValidationResult("Payment token or license is required.");
+ }
+ }
}
}
diff --git a/src/Core/Services/ILicenseVerificationService.cs b/src/Core/Services/ILicenseVerificationService.cs
index 3a077fa1e0..0534844905 100644
--- a/src/Core/Services/ILicenseVerificationService.cs
+++ b/src/Core/Services/ILicenseVerificationService.cs
@@ -1,4 +1,5 @@
-using Bit.Core.Models.Table;
+using Bit.Core.Models.Business;
+using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
@@ -6,5 +7,6 @@ namespace Bit.Core.Services
{
bool VerifyOrganizationPlan(Organization organization);
bool VerifyUserPremium(User user);
+ bool VerifyLicense(ILicense license);
}
}
diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs
index 7016a1427a..a51f04199c 100644
--- a/src/Core/Services/IUserService.cs
+++ b/src/Core/Services/IUserService.cs
@@ -40,7 +40,7 @@ namespace Bit.Core.Services
Task DeleteAsync(User user);
Task DeleteAsync(User user, string token);
Task SendDeleteConfirmationAsync(string email);
- Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb);
+ Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb, UserLicense license);
Task AdjustStorageAsync(User user, short storageAdjustmentGb);
Task ReplacePaymentMethodAsync(User user, string paymentToken);
Task CancelPremiumAsync(User user, bool endOfPeriod = false);
diff --git a/src/Core/Services/Implementations/RsaLicenseVerificationService.cs b/src/Core/Services/Implementations/RsaLicenseVerificationService.cs
index d684707d97..2a377f5eca 100644
--- a/src/Core/Services/Implementations/RsaLicenseVerificationService.cs
+++ b/src/Core/Services/Implementations/RsaLicenseVerificationService.cs
@@ -28,8 +28,9 @@ namespace Bit.Core.Services
}
_globalSettings = globalSettings;
- _certificate = CoreHelpers.GetCertificate("licensing.crt", null);
- if(false && !_certificate.Thumbprint.Equals(""))
+ _certificate = CoreHelpers.GetEmbeddedCertificate("licensing.cer", null);
+ if(!_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(
+ "207e64a231e8aa32aaf68a61037c075ebebd553f"), StringComparison.InvariantCultureIgnoreCase))
{
throw new Exception("Invalid licensing certificate.");
}
@@ -62,6 +63,11 @@ namespace Bit.Core.Services
return license != null && license.VerifyData(user) && license.VerifySignature(_certificate);
}
+ public bool VerifyLicense(ILicense license)
+ {
+ return license.VerifySignature(_certificate);
+ }
+
private UserLicense ReadUserLicense(User user)
{
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index 302e47e79a..7821811908 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -17,6 +17,8 @@ using U2F.Core.Models;
using U2F.Core.Utils;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
+using System.IO;
+using Newtonsoft.Json;
namespace Bit.Core.Services
{
@@ -35,6 +37,7 @@ namespace Bit.Core.Services
private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher _passwordHasher;
private readonly IEnumerable> _passwordValidators;
+ private readonly ILicenseVerificationService _licenseVerificationService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
@@ -54,6 +57,7 @@ namespace Bit.Core.Services
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger> logger,
+ ILicenseVerificationService licenseVerificationService,
CurrentContext currentContext,
GlobalSettings globalSettings)
: base(
@@ -77,6 +81,7 @@ namespace Bit.Core.Services
_identityErrorDescriber = errors;
_passwordHasher = passwordHasher;
_passwordValidators = passwordValidators;
+ _licenseVerificationService = licenseVerificationService;
_currentContext = currentContext;
_globalSettings = globalSettings;
}
@@ -481,7 +486,7 @@ namespace Bit.Core.Services
if(string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))
{
- user.TwoFactorRecoveryCode = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
+ user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
}
await SaveUserAsync(user);
}
@@ -519,13 +524,13 @@ namespace Bit.Core.Services
}
user.TwoFactorProviders = null;
- user.TwoFactorRecoveryCode = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
+ user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
await SaveUserAsync(user);
return true;
}
- public async Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb)
+ public async Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb, UserLicense license)
{
if(user.Premium)
{
@@ -533,19 +538,36 @@ namespace Bit.Core.Services
}
IPaymentService paymentService = null;
- if(paymentToken.StartsWith("tok_"))
+ if(_globalSettings.SelfHosted)
{
- paymentService = new StripePaymentService();
+ if(license == null || !_licenseVerificationService.VerifyLicense(license))
+ {
+ throw new BadRequestException("Invalid license.");
+ }
+
+ Directory.CreateDirectory(_globalSettings.LicenseDirectory);
+ File.WriteAllText(_globalSettings.LicenseDirectory, JsonConvert.SerializeObject(license, Formatting.Indented));
+ }
+ else if(!string.IsNullOrWhiteSpace(paymentToken))
+ {
+ if(paymentToken.StartsWith("tok_"))
+ {
+ paymentService = new StripePaymentService();
+ }
+ else
+ {
+ paymentService = new BraintreePaymentService(_globalSettings);
+ }
+
+ await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
}
else
{
- paymentService = new BraintreePaymentService(_globalSettings);
+ throw new InvalidOperationException("License or payment token is required.");
}
- await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
-
user.Premium = true;
- user.MaxStorageGb = (short)(1 + additionalStorageGb);
+ user.MaxStorageGb = _globalSettings.SelfHosted ? (short)10240 : (short)(1 + additionalStorageGb);
user.RevisionDate = DateTime.UtcNow;
try
@@ -554,7 +576,11 @@ namespace Bit.Core.Services
}
catch
{
- await paymentService.CancelAndRecoverChargesAsync(user);
+ if(!_globalSettings.SelfHosted)
+ {
+ await paymentService.CancelAndRecoverChargesAsync(user);
+ }
+
throw;
}
}
diff --git a/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs b/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs
index b88cea21df..fb82b894b8 100644
--- a/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs
+++ b/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs
@@ -1,6 +1,7 @@
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Hosting;
using System;
+using Bit.Core.Models.Business;
namespace Bit.Core.Services
{
@@ -16,6 +17,11 @@ namespace Bit.Core.Services
}
}
+ public bool VerifyLicense(ILicense license)
+ {
+ return true;
+ }
+
public bool VerifyOrganizationPlan(Organization organization)
{
return true;
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index 98c7e36b72..c732ca1f11 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -1,11 +1,11 @@
using Bit.Core.Models.Data;
-using Bit.Core.Models.Table;
-using Dapper;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
+using System.IO;
using System.Linq;
+using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -96,11 +96,16 @@ namespace Bit.Core.Utilities
return table;
}
- public static X509Certificate2 GetCertificate(string thumbprint)
+ public static string CleanCertificateThumbprint(string thumbprint)
{
// Clean possible garbage characters from thumbprint copy/paste
// ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint
- thumbprint = Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper();
+ return Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper();
+ }
+
+ public static X509Certificate2 GetCertificate(string thumbprint)
+ {
+ thumbprint = CleanCertificateThumbprint(thumbprint);
X509Certificate2 cert = null;
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
@@ -120,6 +125,17 @@ namespace Bit.Core.Utilities
return new X509Certificate2(file, password);
}
+ public static X509Certificate2 GetEmbeddedCertificate(string file, string password)
+ {
+ var assembly = typeof(CoreHelpers).GetTypeInfo().Assembly;
+ using(var s = assembly.GetManifestResourceStream($"Bit.Core.{file}"))
+ using(var ms = new MemoryStream())
+ {
+ s.CopyTo(ms);
+ return new X509Certificate2(ms.ToArray(), password);
+ }
+ }
+
public static long ToEpocMilliseconds(DateTime date)
{
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
diff --git a/src/Api/licensing.cer b/src/Core/licensing.cer
similarity index 100%
rename from src/Api/licensing.cer
rename to src/Core/licensing.cer
diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj
index e962ebd5a8..d8e8b58bad 100644
--- a/src/Identity/Identity.csproj
+++ b/src/Identity/Identity.csproj
@@ -9,14 +9,6 @@
..\..\docker\Docker.dcproj
-
-
-
-
-
-
-
-
diff --git a/src/Identity/licensing.cer b/src/Identity/licensing.cer
deleted file mode 100644
index 0dbb09c3c6..0000000000
Binary files a/src/Identity/licensing.cer and /dev/null differ