1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

more premium licensing

This commit is contained in:
Kyle Spearrin
2017-08-11 22:55:25 -04:00
parent 73029f76d2
commit 9c254a7325
12 changed files with 126 additions and 59 deletions

View File

@ -25,6 +25,7 @@ namespace Bit.Api.Controllers
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ILicensingService _licenseService;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
@ -32,6 +33,7 @@ namespace Bit.Api.Controllers
IUserService userService, IUserService userService,
ICipherService cipherService, ICipherService cipherService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ILicensingService licenseService,
UserManager<User> userManager, UserManager<User> userManager,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
@ -39,6 +41,7 @@ namespace Bit.Api.Controllers
_cipherService = cipherService; _cipherService = cipherService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_userManager = userManager; _userManager = userManager;
_licenseService = licenseService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
@ -391,7 +394,7 @@ namespace Bit.Api.Controllers
var valid = model.Validate(_globalSettings); var valid = model.Validate(_globalSettings);
UserLicense license = null; UserLicense license = null;
if(valid && model.License != null) if(valid && _globalSettings.SelfHosted && model.License != null)
{ {
try try
{ {
@ -434,7 +437,7 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
return new BillingResponseModel(user, billingInfo); return new BillingResponseModel(user, billingInfo, _licenseService);
} }
[HttpPut("payment")] [HttpPut("payment")]

View File

@ -2,24 +2,25 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Stripe;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
public class BillingResponseModel : ResponseModel public class BillingResponseModel : ResponseModel
{ {
public BillingResponseModel(IStorable storable, BillingInfo billing) public BillingResponseModel(User user, BillingInfo billing, ILicensingService licenseService)
: base("billing") : base("billing")
{ {
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null; Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null;
Charges = billing.Charges.Select(c => new BillingCharge(c)); Charges = billing.Charges.Select(c => new BillingCharge(c));
UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null; UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoice(billing.UpcomingInvoice) : null;
StorageName = storable.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(storable.Storage.Value) : null; StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
StorageGb = storable.Storage.HasValue ? Math.Round(storable.Storage.Value / 1073741824D, 2) : 0; // 1 GB StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
MaxStorageGb = storable.MaxStorageGb; MaxStorageGb = user.MaxStorageGb;
License = new UserLicense(user, billing, licenseService);
} }
public string StorageName { get; set; } public string StorageName { get; set; }
@ -29,6 +30,7 @@ namespace Bit.Core.Models.Api
public BillingSubscription Subscription { get; set; } public BillingSubscription Subscription { get; set; }
public BillingInvoice UpcomingInvoice { get; set; } public BillingInvoice UpcomingInvoice { get; set; }
public IEnumerable<BillingCharge> Charges { get; set; } public IEnumerable<BillingCharge> Charges { get; set; }
public UserLicense License { get; set; }
} }
public class BillingSource public class BillingSource

View File

@ -8,10 +8,11 @@ namespace Bit.Core.Models.Business
string LicenseKey { get; set; } string LicenseKey { get; set; }
int Version { get; set; } int Version { get; set; }
DateTime Issued { get; set; } DateTime Issued { get; set; }
DateTime Expires { get; set; } DateTime? Expires { get; set; }
bool Trial { get; set; } bool Trial { get; set; }
string Signature { get; set; } string Signature { get; set; }
byte[] GetSignatureData(); byte[] GetSignatureData();
bool VerifySignature(X509Certificate2 certificate); bool VerifySignature(X509Certificate2 certificate);
byte[] Sign(X509Certificate2 certificate);
} }
} }

View File

@ -43,7 +43,7 @@ namespace Bit.Core.Models.Business
public bool SelfHost { get; set; } public bool SelfHost { get; set; }
public int Version { get; set; } public int Version { get; set; }
public DateTime Issued { get; set; } public DateTime Issued { get; set; }
public DateTime Expires { get; set; } public DateTime? Expires { get; set; }
public bool Trial { get; set; } public bool Trial { get; set; }
public string Signature { get; set; } public string Signature { get; set; }
public byte[] SignatureBytes => Convert.FromBase64String(Signature); 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}", data = string.Format("organization:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}_{8}_{9}_{10}_{11}_{12}_{13}",
Version, Version,
Utilities.CoreHelpers.ToEpocMilliseconds(Issued), Utilities.CoreHelpers.ToEpocSeconds(Issued),
Utilities.CoreHelpers.ToEpocMilliseconds(Expires), Expires.HasValue ? Utilities.CoreHelpers.ToEpocSeconds(Expires.Value).ToString() : null,
LicenseKey, LicenseKey,
Id, Id,
Enabled, Enabled,
@ -76,7 +76,7 @@ namespace Bit.Core.Models.Business
return Encoding.UTF8.GetBytes(data); return Encoding.UTF8.GetBytes(data);
} }
public bool VerifyData(Organization organization) public bool VerifyData(Organization organization)
{ {
if(Issued > DateTime.UtcNow) if(Issued > DateTime.UtcNow)
@ -115,5 +115,10 @@ namespace Bit.Core.Models.Business
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
} }
} }
public byte[] Sign(X509Certificate2 certificate)
{
throw new NotImplementedException();
}
} }
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Services;
using Newtonsoft.Json;
using System; using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@ -11,12 +13,19 @@ namespace Bit.Core.Models.Business
public UserLicense() public UserLicense()
{ } { }
public UserLicense(User user) public UserLicense(User user, BillingInfo billingInfo, ILicensingService licenseService)
{ {
LicenseKey = ""; LicenseKey = user.LicenseKey;
Id = user.Id; Id = user.Id;
Email = user.Email; Email = user.Email;
Version = 1; 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; } public string LicenseKey { get; set; }
@ -26,9 +35,10 @@ namespace Bit.Core.Models.Business
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; set; }
public int Version { get; set; } public int Version { get; set; }
public DateTime Issued { get; set; } public DateTime Issued { get; set; }
public DateTime Expires { get; set; } public DateTime? Expires { get; set; }
public bool Trial { get; set; } public bool Trial { get; set; }
public string Signature { get; set; } public string Signature { get; set; }
[JsonIgnore]
public byte[] SignatureBytes => Convert.FromBase64String(Signature); public byte[] SignatureBytes => Convert.FromBase64String(Signature);
public byte[] GetSignatureData() public byte[] GetSignatureData()
@ -36,11 +46,12 @@ namespace Bit.Core.Models.Business
string data = null; string data = null;
if(Version == 1) 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, Version,
Utilities.CoreHelpers.ToEpocMilliseconds(Issued), Utilities.CoreHelpers.ToEpocSeconds(Issued),
Utilities.CoreHelpers.ToEpocMilliseconds(Expires), Expires.HasValue ? Utilities.CoreHelpers.ToEpocSeconds(Expires.Value).ToString() : null,
LicenseKey, LicenseKey,
Trial,
Id, Id,
Email, Email,
Premium, Premium,
@ -86,5 +97,18 @@ namespace Bit.Core.Models.Business
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 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);
}
}
} }
} }

View File

@ -3,10 +3,11 @@ using Bit.Core.Models.Table;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public interface ILicenseVerificationService public interface ILicensingService
{ {
bool VerifyOrganizationPlan(Organization organization); bool VerifyOrganizationPlan(Organization organization);
bool VerifyUserPremium(User user); bool VerifyUserPremium(User user);
bool VerifyLicense(ILicense license); bool VerifyLicense(ILicense license);
byte[] SignLicense(ILicense license);
} }
} }

View File

@ -106,7 +106,7 @@ namespace Bit.Core.Services
throw new InvalidOperationException("No exp in token."); 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; return DateTime.UtcNow < expiration;
} }

View File

@ -11,26 +11,23 @@ using System.Text;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class RsaLicenseVerificationService : ILicenseVerificationService public class RsaLicensingService : ILicensingService
{ {
private readonly X509Certificate2 _certificate; private readonly X509Certificate2 _certificate;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private IDictionary<string, UserLicense> _userLicenseCache; private IDictionary<string, UserLicense> _userLicenseCache;
private IDictionary<string, OrganizationLicense> _organizationLicenseCache; private IDictionary<string, OrganizationLicense> _organizationLicenseCache;
public RsaLicenseVerificationService( public RsaLicensingService(
IHostingEnvironment environment, IHostingEnvironment environment,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
if(!environment.IsDevelopment() && !globalSettings.SelfHosted) var certThumbprint = "207e64a231e8aa32aaf68a61037c075ebebd553f";
{
throw new Exception($"{nameof(RsaLicenseVerificationService)} can only be used for self hosted instances.");
}
_globalSettings = globalSettings; _globalSettings = globalSettings;
_certificate = CoreHelpers.GetEmbeddedCertificate("licensing.cer", null); _certificate = !_globalSettings.SelfHosted ? CoreHelpers.GetCertificate(certThumbprint)
if(!_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint( : CoreHelpers.GetEmbeddedCertificate("licensing.cer", null);
"207e64a231e8aa32aaf68a61037c075ebebd553f"), StringComparison.InvariantCultureIgnoreCase)) if(_certificate == null || !_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(certThumbprint),
StringComparison.InvariantCultureIgnoreCase))
{ {
throw new Exception("Invalid licensing certificate."); throw new Exception("Invalid licensing certificate.");
} }
@ -43,7 +40,12 @@ namespace Bit.Core.Services
public bool VerifyOrganizationPlan(Organization organization) public bool VerifyOrganizationPlan(Organization organization)
{ {
if(_globalSettings.SelfHosted && !organization.SelfHost) if(!_globalSettings.SelfHosted)
{
return true;
}
if(!organization.SelfHost)
{ {
return false; return false;
} }
@ -54,6 +56,11 @@ namespace Bit.Core.Services
public bool VerifyUserPremium(User user) public bool VerifyUserPremium(User user)
{ {
if(!_globalSettings.SelfHosted)
{
return user.Premium;
}
if(!user.Premium) if(!user.Premium)
{ {
return false; return false;
@ -68,6 +75,16 @@ namespace Bit.Core.Services
return license.VerifySignature(_certificate); 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) private UserLicense ReadUserLicense(User user)
{ {
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey)) if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
@ -75,7 +92,7 @@ namespace Bit.Core.Services
return _userLicenseCache[user.LicenseKey]; return _userLicenseCache[user.LicenseKey];
} }
var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.LicenseKey}.json"; var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.Id}.json";
if(!File.Exists(filePath)) if(!File.Exists(filePath))
{ {
return null; return null;
@ -98,7 +115,7 @@ namespace Bit.Core.Services
return _organizationLicenseCache[organization.LicenseKey]; return _organizationLicenseCache[organization.LicenseKey];
} }
var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.LicenseKey}.json"; var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.Id}.json";
if(!File.Exists(filePath)) if(!File.Exists(filePath))
{ {
return null; return null;

View File

@ -37,7 +37,7 @@ namespace Bit.Core.Services
private readonly IdentityOptions _identityOptions; private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher<User> _passwordHasher; private readonly IPasswordHasher<User> _passwordHasher;
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators; private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
private readonly ILicenseVerificationService _licenseVerificationService; private readonly ILicensingService _licenseService;
private readonly CurrentContext _currentContext; private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
@ -57,7 +57,7 @@ namespace Bit.Core.Services
IdentityErrorDescriber errors, IdentityErrorDescriber errors,
IServiceProvider services, IServiceProvider services,
ILogger<UserManager<User>> logger, ILogger<UserManager<User>> logger,
ILicenseVerificationService licenseVerificationService, ILicensingService licenseService,
CurrentContext currentContext, CurrentContext currentContext,
GlobalSettings globalSettings) GlobalSettings globalSettings)
: base( : base(
@ -81,7 +81,7 @@ namespace Bit.Core.Services
_identityErrorDescriber = errors; _identityErrorDescriber = errors;
_passwordHasher = passwordHasher; _passwordHasher = passwordHasher;
_passwordValidators = passwordValidators; _passwordValidators = passwordValidators;
_licenseVerificationService = licenseVerificationService; _licenseService = licenseService;
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
@ -540,13 +540,14 @@ namespace Bit.Core.Services
IPaymentService paymentService = null; IPaymentService paymentService = null;
if(_globalSettings.SelfHosted) if(_globalSettings.SelfHosted)
{ {
if(license == null || !_licenseVerificationService.VerifyLicense(license)) if(license == null || !_licenseService.VerifyLicense(license))
{ {
throw new BadRequestException("Invalid license."); throw new BadRequestException("Invalid license.");
} }
Directory.CreateDirectory(_globalSettings.LicenseDirectory); var dir = $"{_globalSettings.LicenseDirectory}/user";
File.WriteAllText(_globalSettings.LicenseDirectory, JsonConvert.SerializeObject(license, Formatting.Indented)); Directory.CreateDirectory(dir);
File.WriteAllText($"{dir}/{user.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented));
} }
else if(!string.IsNullOrWhiteSpace(paymentToken)) else if(!string.IsNullOrWhiteSpace(paymentToken))
{ {
@ -567,20 +568,26 @@ namespace Bit.Core.Services
} }
user.Premium = true; user.Premium = true;
user.MaxStorageGb = _globalSettings.SelfHosted ? (short)10240 : (short)(1 + additionalStorageGb);
user.RevisionDate = DateTime.UtcNow; 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 try
{ {
await SaveUserAsync(user); await SaveUserAsync(user);
} }
catch catch when(!_globalSettings.SelfHosted)
{ {
if(!_globalSettings.SelfHosted) await paymentService.CancelAndRecoverChargesAsync(user);
{
await paymentService.CancelAndRecoverChargesAsync(user);
}
throw; throw;
} }
} }

View File

@ -5,15 +5,15 @@ using Bit.Core.Models.Business;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class NoopLicenseVerificationService : ILicenseVerificationService public class NoopLicensingService : ILicensingService
{ {
public NoopLicenseVerificationService( public NoopLicensingService(
IHostingEnvironment environment, IHostingEnvironment environment,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
if(!environment.IsDevelopment() && globalSettings.SelfHosted) 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; return user.Premium;
} }
public byte[] SignLicense(ILicense license)
{
return new byte[0];
}
} }
} }

View File

@ -146,6 +146,16 @@ namespace Bit.Core.Utilities
return _epoc.AddMilliseconds(milliseconds); 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) public static string U2fAppIdUrl(GlobalSettings globalSettings)
{ {
return string.Concat(globalSettings.BaseServiceUri.Vault, "/app-id.json"); return string.Concat(globalSettings.BaseServiceUri.Vault, "/app-id.json");

View File

@ -55,6 +55,7 @@ namespace Bit.Core.Utilities
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
{ {
services.AddSingleton<IMailService, RazorViewMailService>(); services.AddSingleton<IMailService, RazorViewMailService>();
services.AddSingleton<ILicensingService, RsaLicensingService>();
if(CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey)) if(CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey))
{ {
@ -113,15 +114,6 @@ namespace Bit.Core.Utilities
{ {
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>(); services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
} }
if(globalSettings.SelfHosted)
{
services.AddSingleton<ILicenseVerificationService, RsaLicenseVerificationService>();
}
else
{
services.AddSingleton<ILicenseVerificationService, NoopLicenseVerificationService>();
}
} }
public static void AddNoopServices(this IServiceCollection services) public static void AddNoopServices(this IServiceCollection services)
@ -132,7 +124,7 @@ namespace Bit.Core.Utilities
services.AddSingleton<IBlockIpService, NoopBlockIpService>(); services.AddSingleton<IBlockIpService, NoopBlockIpService>();
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>(); services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>(); services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
services.AddSingleton<ILicenseVerificationService, NoopLicenseVerificationService>(); services.AddSingleton<ILicensingService, NoopLicensingService>();
} }
public static IdentityBuilder AddCustomIdentityServices( public static IdentityBuilder AddCustomIdentityServices(