From 73029f76d22bc7ee7aa40a9f579f54a3553ffafc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 11 Aug 2017 17:06:31 -0400 Subject: [PATCH] premium signup with license file --- src/Api/Api.csproj | 8 --- src/Api/Controllers/AccountsController.cs | 32 +++++++++++- src/Billing/Billing.csproj | 4 -- src/Billing/licensing.cer | Bin 1303 -> 0 bytes src/Core/Core.csproj | 1 + .../Request/Accounts/PremiumRequestModel.cs | 22 ++++++++- .../Services/ILicenseVerificationService.cs | 4 +- src/Core/Services/IUserService.cs | 2 +- .../RsaLicenseVerificationService.cs | 10 +++- .../Services/Implementations/UserService.cs | 46 ++++++++++++++---- .../NoopLicenseVerificationService.cs | 6 +++ src/Core/Utilities/CoreHelpers.cs | 24 +++++++-- src/{Api => Core}/licensing.cer | Bin src/Identity/Identity.csproj | 8 --- src/Identity/licensing.cer | Bin 1303 -> 0 bytes 15 files changed, 125 insertions(+), 42 deletions(-) delete mode 100644 src/Billing/licensing.cer rename src/{Api => Core}/licensing.cer (100%) delete mode 100644 src/Identity/licensing.cer 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 0dbb09c3c6e070d788dc84985ef36f955b9758bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1303 zcmXqLVih)MV*0&+nTe5!iId^=m%=$KUe0VX;AP{~YV&CO&dbQi&B|b)U?^uG!^RxS z!ptL@lvz@qSd@~Qr{I&BoSIjhs^F4ZW*{fdYiMp@VPI)!YGh(;5+%-S0^%A$xkQ-I z#H565Eh8%fa}yIk1JHR~OifIT46pCa*%`=O{^I=xv(xwgeHGQX`$6A@(Pa0xyScqn zbzbf&5#tcZ;5cU)wlsYp%svmnWXbokO1e zRtuSDAmv?UDYu|9RB+kxu*E4I$9~S#4&CLnew)b?t?eogD<=Fi3u25eF!$Q?h4Iv8 zyMUfn-FB^m3xd>{4k-V!+sKtS!E(vWjGY(moe0>v_UOjg{NqO7&y=3Ep0zu$YEt9( z!py_RE7JUrcXEEY_vWF$f!y@x*Lf|!^h-9`ED>qvC`)PIGAm+h$#35257uncm)`c! zT#!dtJ-%JN>&R2Z9*-B|@2XGcYcreQiTG3Y;Mcm?3zzfvpDq1RdTq@fxkhc)HSPCB zf{y(z{NliP)ytsE>wJu0`1Mx@WA;ppE@krkY8$+s<+bI>hgT)zw)@E^XYZ}Z}xCo?P-emz0h&Pz0(s;hi%$% zO1ks8)Aw^v8lw}srw5*BF~6o-r(#{(rl_*{n%K@afAnX?e^=S%%j(U<%*epFIKaTq zKo*!1W%*ddSVWHgEZB7A?d|6of)^h=**3e6$I0`UfjmfBnMJ}ttO2_M9*_cI7FGjh zM#lfhX%3jwfoYDBAvnvg>o`M^+u63=CT?us9E9%ud1Q4!C9yr+IH!ElmE>z|^6R%b zh@Zc*%wpn0_5I>YXNdCKTl;cP->@Z9W$Mf+yV4ie_&XlaV0JZA(&@19s(X|4J1X2@ zwZf0@n{HX=Y4EOCd!m}1{f^k2$nKaRfo(poJ@)2j?G9h_G+7$yn7KOLR)v=dP7B*E5biFSv&3;EiZ1T^ehge@8G5ddf+4PGOj5S$!g*fy@ znHDY4@zt9mo@lCjQ{P|XMxEaGV=~L1&JbCyQ?tAOsY->B+`Ej2qQ^fPtiSEO;yu?lMd|tzo^%MwycV6!?rdK9X??PdvZ-2$#0UEu#2cX;ZH7xVi#PE~L6I=}Uf=(pcIKX?lS4NaE1=r~%}e7JNpsjOj&@{PWa zCfzRQbFPW=-Tib%M(q1N5k1RqXYN{WRzEl4&7{Djxte=6trgFekT7pn2|mMTQWRhM zYx{cXK+DIaCDM#yEVpJ&Rn%8(?Yz|b%{aR{f|WH#ZnJ}a#P`j!@6}d5ecU?rUVH35 i + 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 0dbb09c3c6e070d788dc84985ef36f955b9758bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1303 zcmXqLVih)MV*0&+nTe5!iId^=m%=$KUe0VX;AP{~YV&CO&dbQi&B|b)U?^uG!^RxS z!ptL@lvz@qSd@~Qr{I&BoSIjhs^F4ZW*{fdYiMp@VPI)!YGh(;5+%-S0^%A$xkQ-I z#H565Eh8%fa}yIk1JHR~OifIT46pCa*%`=O{^I=xv(xwgeHGQX`$6A@(Pa0xyScqn zbzbf&5#tcZ;5cU)wlsYp%svmnWXbokO1e zRtuSDAmv?UDYu|9RB+kxu*E4I$9~S#4&CLnew)b?t?eogD<=Fi3u25eF!$Q?h4Iv8 zyMUfn-FB^m3xd>{4k-V!+sKtS!E(vWjGY(moe0>v_UOjg{NqO7&y=3Ep0zu$YEt9( z!py_RE7JUrcXEEY_vWF$f!y@x*Lf|!^h-9`ED>qvC`)PIGAm+h$#35257uncm)`c! zT#!dtJ-%JN>&R2Z9*-B|@2XGcYcreQiTG3Y;Mcm?3zzfvpDq1RdTq@fxkhc)HSPCB zf{y(z{NliP)ytsE>wJu0`1Mx@WA;ppE@krkY8$+s<+bI>hgT)zw)@E^XYZ}Z}xCo?P-emz0h&Pz0(s;hi%$% zO1ks8)Aw^v8lw}srw5*BF~6o-r(#{(rl_*{n%K@afAnX?e^=S%%j(U<%*epFIKaTq zKo*!1W%*ddSVWHgEZB7A?d|6of)^h=**3e6$I0`UfjmfBnMJ}ttO2_M9*_cI7FGjh zM#lfhX%3jwfoYDBAvnvg>o`M^+u63=CT?us9E9%ud1Q4!C9yr+IH!ElmE>z|^6R%b zh@Zc*%wpn0_5I>YXNdCKTl;cP->@Z9W$Mf+yV4ie_&XlaV0JZA(&@19s(X|4J1X2@ zwZf0@n{HX=Y4EOCd!m}1{f^k2$nKaRfo(poJ@)2j?G9h_G+7$yn7KOLR)v=dP7B*E5biFSv&3;EiZ1T^ehge@8G5ddf+4PGOj5S$!g*fy@ znHDY4@zt9mo@lCjQ{P|XMxEaGV=~L1&JbCyQ?tAOsY->B+`Ej2qQ^fPtiSEO;yu?lMd|tzo^%MwycV6!?rdK9X??PdvZ-2$#0UEu#2cX;ZH7xVi#PE~L6I=}Uf=(pcIKX?lS4NaE1=r~%}e7JNpsjOj&@{PWa zCfzRQbFPW=-Tib%M(q1N5k1RqXYN{WRzEl4&7{Djxte=6trgFekT7pn2|mMTQWRhM zYx{cXK+DIaCDM#yEVpJ&Rn%8(?Yz|b%{aR{f|WH#ZnJ}a#P`j!@6}d5ecU?rUVH35 i