diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 892e969e8d..52475e8718 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -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 _userManager; private readonly GlobalSettings _globalSettings; @@ -32,6 +33,7 @@ namespace Bit.Api.Controllers IUserService userService, ICipherService cipherService, IOrganizationUserRepository organizationUserRepository, + ILicensingService licenseService, UserManager 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")] diff --git a/src/Core/Models/Api/Response/BillingResponseModel.cs b/src/Core/Models/Api/Response/BillingResponseModel.cs index a59e7c4355..b71352472e 100644 --- a/src/Core/Models/Api/Response/BillingResponseModel.cs +++ b/src/Core/Models/Api/Response/BillingResponseModel.cs @@ -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 Charges { get; set; } + public UserLicense License { get; set; } } public class BillingSource diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index 5dc8470f1c..b1388f04a4 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -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); } } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index d42f6b3c17..4dc847d85b 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -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(); + } } } diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 3320a19b6a..17032dc414 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -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); + } + } } } diff --git a/src/Core/Services/ILicenseVerificationService.cs b/src/Core/Services/ILicensingService.cs similarity index 75% rename from src/Core/Services/ILicenseVerificationService.cs rename to src/Core/Services/ILicensingService.cs index 0534844905..c82650eda1 100644 --- a/src/Core/Services/ILicenseVerificationService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -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); } } diff --git a/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs b/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs index a604308fe0..c8701b1370 100644 --- a/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs +++ b/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs @@ -106,7 +106,7 @@ namespace Bit.Core.Services throw new InvalidOperationException("No exp in token."); } - var expiration = CoreHelpers.FromEpocMilliseconds(1000 * exp.Value()); + var expiration = CoreHelpers.FromEpocSeconds(exp.Value()); return DateTime.UtcNow < expiration; } diff --git a/src/Core/Services/Implementations/RsaLicenseVerificationService.cs b/src/Core/Services/Implementations/RsaLicensingService.cs similarity index 75% rename from src/Core/Services/Implementations/RsaLicenseVerificationService.cs rename to src/Core/Services/Implementations/RsaLicensingService.cs index 2a377f5eca..3742a2a7f6 100644 --- a/src/Core/Services/Implementations/RsaLicenseVerificationService.cs +++ b/src/Core/Services/Implementations/RsaLicensingService.cs @@ -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 _userLicenseCache; private IDictionary _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; diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 7821811908..6dcf2f5a4d 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -37,7 +37,7 @@ namespace Bit.Core.Services private readonly IdentityOptions _identityOptions; private readonly IPasswordHasher _passwordHasher; private readonly IEnumerable> _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> 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; } } diff --git a/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs similarity index 68% rename from src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs rename to src/Core/Services/NoopImplementations/NoopLicensingService.cs index fb82b894b8..e738dcfd5d 100644 --- a/src/Core/Services/NoopImplementations/NoopLicenseVerificationService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -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]; + } } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index c732ca1f11..1b88e1e003 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -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"); diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index c4de428d52..c1b497de56 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -55,6 +55,7 @@ namespace Bit.Core.Utilities public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) { services.AddSingleton(); + services.AddSingleton(); if(CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey)) { @@ -113,15 +114,6 @@ namespace Bit.Core.Utilities { services.AddSingleton(); } - - if(globalSettings.SelfHosted) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } } public static void AddNoopServices(this IServiceCollection services) @@ -132,7 +124,7 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); } public static IdentityBuilder AddCustomIdentityServices(