1
0
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:
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 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")]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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