mirror of
https://github.com/bitwarden/server.git
synced 2025-02-26 03:31:34 +01:00
more premium licensing
This commit is contained in:
parent
73029f76d2
commit
9c254a7325
@ -25,6 +25,7 @@ namespace Bit.Api.Controllers
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILicensingService _licenseService;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
@ -32,6 +33,7 @@ namespace Bit.Api.Controllers
|
||||
IUserService userService,
|
||||
ICipherService cipherService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ILicensingService licenseService,
|
||||
UserManager<User> userManager,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
@ -39,6 +41,7 @@ namespace Bit.Api.Controllers
|
||||
_cipherService = cipherService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_userManager = userManager;
|
||||
_licenseService = licenseService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
@ -391,7 +394,7 @@ namespace Bit.Api.Controllers
|
||||
|
||||
var valid = model.Validate(_globalSettings);
|
||||
UserLicense license = null;
|
||||
if(valid && model.License != null)
|
||||
if(valid && _globalSettings.SelfHosted && model.License != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -434,7 +437,7 @@ namespace Bit.Api.Controllers
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new BillingResponseModel(user, billingInfo);
|
||||
return new BillingResponseModel(user, billingInfo, _licenseService);
|
||||
}
|
||||
|
||||
[HttpPut("payment")]
|
||||
|
@ -2,24 +2,25 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class BillingResponseModel : ResponseModel
|
||||
{
|
||||
public BillingResponseModel(IStorable storable, BillingInfo billing)
|
||||
public BillingResponseModel(User user, BillingInfo billing, ILicensingService licenseService)
|
||||
: base("billing")
|
||||
{
|
||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
|
||||
Charges = billing.Charges.Select(c => new BillingCharge(c));
|
||||
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
|
||||
StorageName = storable.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(storable.Storage.Value) : null;
|
||||
StorageGb = storable.Storage.HasValue ? Math.Round(storable.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
MaxStorageGb = storable.MaxStorageGb;
|
||||
StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
|
||||
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
MaxStorageGb = user.MaxStorageGb;
|
||||
License = new UserLicense(user, billing, licenseService);
|
||||
}
|
||||
|
||||
public string StorageName { get; set; }
|
||||
@ -29,6 +30,7 @@ namespace Bit.Core.Models.Api
|
||||
public BillingSubscription Subscription { get; set; }
|
||||
public BillingInvoice UpcomingInvoice { get; set; }
|
||||
public IEnumerable<BillingCharge> Charges { get; set; }
|
||||
public UserLicense License { get; set; }
|
||||
}
|
||||
|
||||
public class BillingSource
|
||||
|
@ -8,10 +8,11 @@ namespace Bit.Core.Models.Business
|
||||
string LicenseKey { get; set; }
|
||||
int Version { get; set; }
|
||||
DateTime Issued { get; set; }
|
||||
DateTime Expires { get; set; }
|
||||
DateTime? Expires { get; set; }
|
||||
bool Trial { get; set; }
|
||||
string Signature { get; set; }
|
||||
byte[] GetSignatureData();
|
||||
bool VerifySignature(X509Certificate2 certificate);
|
||||
byte[] Sign(X509Certificate2 certificate);
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace Bit.Core.Models.Business
|
||||
public bool SelfHost { get; set; }
|
||||
public int Version { get; set; }
|
||||
public DateTime Issued { get; set; }
|
||||
public DateTime Expires { get; set; }
|
||||
public DateTime? Expires { get; set; }
|
||||
public bool Trial { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
@ -55,8 +55,8 @@ namespace Bit.Core.Models.Business
|
||||
{
|
||||
data = string.Format("organization:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}_{8}_{9}_{10}_{11}_{12}_{13}",
|
||||
Version,
|
||||
Utilities.CoreHelpers.ToEpocMilliseconds(Issued),
|
||||
Utilities.CoreHelpers.ToEpocMilliseconds(Expires),
|
||||
Utilities.CoreHelpers.ToEpocSeconds(Issued),
|
||||
Expires.HasValue ? Utilities.CoreHelpers.ToEpocSeconds(Expires.Value).ToString() : null,
|
||||
LicenseKey,
|
||||
Id,
|
||||
Enabled,
|
||||
@ -76,7 +76,7 @@ namespace Bit.Core.Models.Business
|
||||
|
||||
return Encoding.UTF8.GetBytes(data);
|
||||
}
|
||||
|
||||
|
||||
public bool VerifyData(Organization organization)
|
||||
{
|
||||
if(Issued > DateTime.UtcNow)
|
||||
@ -115,5 +115,10 @@ namespace Bit.Core.Models.Business
|
||||
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Sign(X509Certificate2 certificate)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
@ -11,12 +13,19 @@ namespace Bit.Core.Models.Business
|
||||
public UserLicense()
|
||||
{ }
|
||||
|
||||
public UserLicense(User user)
|
||||
public UserLicense(User user, BillingInfo billingInfo, ILicensingService licenseService)
|
||||
{
|
||||
LicenseKey = "";
|
||||
LicenseKey = user.LicenseKey;
|
||||
Id = user.Id;
|
||||
Email = user.Email;
|
||||
Version = 1;
|
||||
Premium = user.Premium;
|
||||
MaxStorageGb = user.MaxStorageGb;
|
||||
Issued = DateTime.UtcNow;
|
||||
Expires = billingInfo?.UpcomingInvoice?.Date;
|
||||
Trial = (billingInfo?.Subscription?.TrialEndDate.HasValue ?? false) &&
|
||||
billingInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;
|
||||
Signature = Convert.ToBase64String(licenseService.SignLicense(this));
|
||||
}
|
||||
|
||||
public string LicenseKey { get; set; }
|
||||
@ -26,9 +35,10 @@ namespace Bit.Core.Models.Business
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public int Version { get; set; }
|
||||
public DateTime Issued { get; set; }
|
||||
public DateTime Expires { get; set; }
|
||||
public DateTime? Expires { get; set; }
|
||||
public bool Trial { get; set; }
|
||||
public string Signature { get; set; }
|
||||
[JsonIgnore]
|
||||
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
|
||||
public byte[] GetSignatureData()
|
||||
@ -36,11 +46,12 @@ namespace Bit.Core.Models.Business
|
||||
string data = null;
|
||||
if(Version == 1)
|
||||
{
|
||||
data = string.Format("user:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}",
|
||||
data = string.Format("user:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}_{8}",
|
||||
Version,
|
||||
Utilities.CoreHelpers.ToEpocMilliseconds(Issued),
|
||||
Utilities.CoreHelpers.ToEpocMilliseconds(Expires),
|
||||
Utilities.CoreHelpers.ToEpocSeconds(Issued),
|
||||
Expires.HasValue ? Utilities.CoreHelpers.ToEpocSeconds(Expires.Value).ToString() : null,
|
||||
LicenseKey,
|
||||
Trial,
|
||||
Id,
|
||||
Email,
|
||||
Premium,
|
||||
@ -86,5 +97,18 @@ namespace Bit.Core.Models.Business
|
||||
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Sign(X509Certificate2 certificate)
|
||||
{
|
||||
if(!certificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException("You don't have the private key!");
|
||||
}
|
||||
|
||||
using(var rsa = certificate.GetRSAPrivateKey())
|
||||
{
|
||||
return rsa.SignData(GetSignatureData(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ using Bit.Core.Models.Table;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface ILicenseVerificationService
|
||||
public interface ILicensingService
|
||||
{
|
||||
bool VerifyOrganizationPlan(Organization organization);
|
||||
bool VerifyUserPremium(User user);
|
||||
bool VerifyLicense(ILicense license);
|
||||
byte[] SignLicense(ILicense license);
|
||||
}
|
||||
}
|
@ -106,7 +106,7 @@ namespace Bit.Core.Services
|
||||
throw new InvalidOperationException("No exp in token.");
|
||||
}
|
||||
|
||||
var expiration = CoreHelpers.FromEpocMilliseconds(1000 * exp.Value<long>());
|
||||
var expiration = CoreHelpers.FromEpocSeconds(exp.Value<long>());
|
||||
return DateTime.UtcNow < expiration;
|
||||
}
|
||||
|
||||
|
@ -11,26 +11,23 @@ using System.Text;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class RsaLicenseVerificationService : ILicenseVerificationService
|
||||
public class RsaLicensingService : ILicensingService
|
||||
{
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private IDictionary<string, UserLicense> _userLicenseCache;
|
||||
private IDictionary<string, OrganizationLicense> _organizationLicenseCache;
|
||||
|
||||
public RsaLicenseVerificationService(
|
||||
public RsaLicensingService(
|
||||
IHostingEnvironment environment,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
if(!environment.IsDevelopment() && !globalSettings.SelfHosted)
|
||||
{
|
||||
throw new Exception($"{nameof(RsaLicenseVerificationService)} can only be used for self hosted instances.");
|
||||
}
|
||||
|
||||
var certThumbprint = "207e64a231e8aa32aaf68a61037c075ebebd553f";
|
||||
_globalSettings = globalSettings;
|
||||
_certificate = CoreHelpers.GetEmbeddedCertificate("licensing.cer", null);
|
||||
if(!_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(
|
||||
"207e64a231e8aa32aaf68a61037c075ebebd553f"), StringComparison.InvariantCultureIgnoreCase))
|
||||
_certificate = !_globalSettings.SelfHosted ? CoreHelpers.GetCertificate(certThumbprint)
|
||||
: CoreHelpers.GetEmbeddedCertificate("licensing.cer", null);
|
||||
if(_certificate == null || !_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(certThumbprint),
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new Exception("Invalid licensing certificate.");
|
||||
}
|
||||
@ -43,7 +40,12 @@ namespace Bit.Core.Services
|
||||
|
||||
public bool VerifyOrganizationPlan(Organization organization)
|
||||
{
|
||||
if(_globalSettings.SelfHosted && !organization.SelfHost)
|
||||
if(!_globalSettings.SelfHosted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!organization.SelfHost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -54,6 +56,11 @@ namespace Bit.Core.Services
|
||||
|
||||
public bool VerifyUserPremium(User user)
|
||||
{
|
||||
if(!_globalSettings.SelfHosted)
|
||||
{
|
||||
return user.Premium;
|
||||
}
|
||||
|
||||
if(!user.Premium)
|
||||
{
|
||||
return false;
|
||||
@ -68,6 +75,16 @@ namespace Bit.Core.Services
|
||||
return license.VerifySignature(_certificate);
|
||||
}
|
||||
|
||||
public byte[] SignLicense(ILicense license)
|
||||
{
|
||||
if(_globalSettings.SelfHosted || !_certificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot sign licenses.");
|
||||
}
|
||||
|
||||
return license.Sign(_certificate);
|
||||
}
|
||||
|
||||
private UserLicense ReadUserLicense(User user)
|
||||
{
|
||||
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
|
||||
@ -75,7 +92,7 @@ namespace Bit.Core.Services
|
||||
return _userLicenseCache[user.LicenseKey];
|
||||
}
|
||||
|
||||
var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.LicenseKey}.json";
|
||||
var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.Id}.json";
|
||||
if(!File.Exists(filePath))
|
||||
{
|
||||
return null;
|
||||
@ -98,7 +115,7 @@ namespace Bit.Core.Services
|
||||
return _organizationLicenseCache[organization.LicenseKey];
|
||||
}
|
||||
|
||||
var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.LicenseKey}.json";
|
||||
var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.Id}.json";
|
||||
if(!File.Exists(filePath))
|
||||
{
|
||||
return null;
|
@ -37,7 +37,7 @@ namespace Bit.Core.Services
|
||||
private readonly IdentityOptions _identityOptions;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
|
||||
private readonly ILicenseVerificationService _licenseVerificationService;
|
||||
private readonly ILicensingService _licenseService;
|
||||
private readonly CurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
@ -57,7 +57,7 @@ namespace Bit.Core.Services
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<User>> logger,
|
||||
ILicenseVerificationService licenseVerificationService,
|
||||
ILicensingService licenseService,
|
||||
CurrentContext currentContext,
|
||||
GlobalSettings globalSettings)
|
||||
: base(
|
||||
@ -81,7 +81,7 @@ namespace Bit.Core.Services
|
||||
_identityErrorDescriber = errors;
|
||||
_passwordHasher = passwordHasher;
|
||||
_passwordValidators = passwordValidators;
|
||||
_licenseVerificationService = licenseVerificationService;
|
||||
_licenseService = licenseService;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
@ -540,13 +540,14 @@ namespace Bit.Core.Services
|
||||
IPaymentService paymentService = null;
|
||||
if(_globalSettings.SelfHosted)
|
||||
{
|
||||
if(license == null || !_licenseVerificationService.VerifyLicense(license))
|
||||
if(license == null || !_licenseService.VerifyLicense(license))
|
||||
{
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_globalSettings.LicenseDirectory);
|
||||
File.WriteAllText(_globalSettings.LicenseDirectory, JsonConvert.SerializeObject(license, Formatting.Indented));
|
||||
var dir = $"{_globalSettings.LicenseDirectory}/user";
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText($"{dir}/{user.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented));
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
@ -567,20 +568,26 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = _globalSettings.SelfHosted ? (short)10240 : (short)(1 + additionalStorageGb);
|
||||
user.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
if(_globalSettings.SelfHosted)
|
||||
{
|
||||
user.MaxStorageGb = 10240;
|
||||
user.LicenseKey = license.LicenseKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.MaxStorageGb = (short)(1 + additionalStorageGb);
|
||||
user.LicenseKey = CoreHelpers.SecureRandomString(20, upper: false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SaveUserAsync(user);
|
||||
}
|
||||
catch
|
||||
catch when(!_globalSettings.SelfHosted)
|
||||
{
|
||||
if(!_globalSettings.SelfHosted)
|
||||
{
|
||||
await paymentService.CancelAndRecoverChargesAsync(user);
|
||||
}
|
||||
|
||||
await paymentService.CancelAndRecoverChargesAsync(user);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,15 @@ using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class NoopLicenseVerificationService : ILicenseVerificationService
|
||||
public class NoopLicensingService : ILicensingService
|
||||
{
|
||||
public NoopLicenseVerificationService(
|
||||
public NoopLicensingService(
|
||||
IHostingEnvironment environment,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
if(!environment.IsDevelopment() && globalSettings.SelfHosted)
|
||||
{
|
||||
throw new Exception($"{nameof(NoopLicenseVerificationService)} cannot be used for self hosted instances.");
|
||||
throw new Exception($"{nameof(NoopLicensingService)} cannot be used for self hosted instances.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,5 +31,10 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return user.Premium;
|
||||
}
|
||||
|
||||
public byte[] SignLicense(ILicense license)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
}
|
@ -146,6 +146,16 @@ namespace Bit.Core.Utilities
|
||||
return _epoc.AddMilliseconds(milliseconds);
|
||||
}
|
||||
|
||||
public static long ToEpocSeconds(DateTime date)
|
||||
{
|
||||
return (long)Math.Round((date - _epoc).TotalSeconds, 0);
|
||||
}
|
||||
|
||||
public static DateTime FromEpocSeconds(long seconds)
|
||||
{
|
||||
return _epoc.AddSeconds(seconds);
|
||||
}
|
||||
|
||||
public static string U2fAppIdUrl(GlobalSettings globalSettings)
|
||||
{
|
||||
return string.Concat(globalSettings.BaseServiceUri.Vault, "/app-id.json");
|
||||
|
@ -55,6 +55,7 @@ namespace Bit.Core.Utilities
|
||||
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
services.AddSingleton<IMailService, RazorViewMailService>();
|
||||
services.AddSingleton<ILicensingService, RsaLicensingService>();
|
||||
|
||||
if(CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey))
|
||||
{
|
||||
@ -113,15 +114,6 @@ namespace Bit.Core.Utilities
|
||||
{
|
||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||
}
|
||||
|
||||
if(globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddSingleton<ILicenseVerificationService, RsaLicenseVerificationService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ILicenseVerificationService, NoopLicenseVerificationService>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddNoopServices(this IServiceCollection services)
|
||||
@ -132,7 +124,7 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<IBlockIpService, NoopBlockIpService>();
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||
services.AddSingleton<ILicenseVerificationService, NoopLicenseVerificationService>();
|
||||
services.AddSingleton<ILicensingService, NoopLicensingService>();
|
||||
}
|
||||
|
||||
public static IdentityBuilder AddCustomIdentityServices(
|
||||
|
Loading…
Reference in New Issue
Block a user