1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

license verification services for user/org

This commit is contained in:
Kyle Spearrin 2017-08-09 17:01:37 -04:00
parent 3deec076c7
commit a1d064ed9e
26 changed files with 457 additions and 2 deletions

View File

@ -9,6 +9,14 @@
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<None Remove="licensing.cer" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="licensing.cer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>

View File

@ -4,7 +4,7 @@
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4000",
"sslPort": 44377
"sslPort": 0
}
},
"profiles": {

BIN
src/Api/licensing.cer Normal file

Binary file not shown.

View File

@ -8,6 +8,10 @@
<UserSecretsId>bitwarden-Billing</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="licensing.cer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>

BIN
src/Billing/licensing.cer Normal file

Binary file not shown.

View File

@ -7,6 +7,7 @@
public virtual string StripeApiKey { get; set; }
public virtual string ProjectName { get; set; }
public virtual string LogDirectory { get; set; }
public virtual string LicenseDirectory { get; set; }
public virtual BaseServiceUriSettings BaseServiceUri { get; set; } = new BaseServiceUriSettings();
public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings();

View File

@ -0,0 +1,17 @@
using System;
using System.Security.Cryptography.X509Certificates;
namespace Bit.Core.Models.Business
{
public interface ILicense
{
string LicenseKey { get; set; }
int Version { get; set; }
DateTime Issued { get; set; }
DateTime Expires { get; set; }
bool Trial { get; set; }
string Signature { get; set; }
byte[] GetSignatureData();
bool VerifySignature(X509Certificate2 certificate);
}
}

View File

@ -0,0 +1,119 @@
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Bit.Core.Models.Business
{
public class OrganizationLicense : ILicense
{
public OrganizationLicense()
{ }
public OrganizationLicense(Organization org)
{
LicenseKey = "";
Id = org.Id;
Name = org.Name;
Enabled = org.Enabled;
Seats = org.Seats;
MaxCollections = org.MaxCollections;
UseGroups = org.UseGroups;
UseDirectory = org.UseDirectory;
UseTotp = org.UseTotp;
MaxStorageGb = org.MaxStorageGb;
SelfHost = org.SelfHost;
Version = 1;
}
public string LicenseKey { get; set; }
public Guid Id { get; set; }
public string Name { get; set; }
public bool Enabled { get; set; }
public string Plan { get; set; }
public PlanType PlanType { get; set; }
public short? Seats { get; set; }
public short? MaxCollections { get; set; }
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseTotp { get; set; }
public short? MaxStorageGb { get; set; }
public bool SelfHost { get; set; }
public int Version { get; set; }
public DateTime Issued { get; set; }
public DateTime Expires { get; set; }
public bool Trial { get; set; }
public string Signature { get; set; }
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
public byte[] GetSignatureData()
{
string data = null;
if(Version == 1)
{
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),
LicenseKey,
Id,
Enabled,
PlanType,
Seats,
MaxCollections,
UseGroups,
UseDirectory,
UseTotp,
MaxStorageGb,
SelfHost);
}
else
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
return Encoding.UTF8.GetBytes(data);
}
public bool VerifyData(Organization organization)
{
if(Issued > DateTime.UtcNow)
{
return false;
}
if(Expires < DateTime.UtcNow)
{
return false;
}
if(Version == 1)
{
return
organization.LicenseKey.Equals(LicenseKey, StringComparison.InvariantCultureIgnoreCase) &&
organization.Enabled == Enabled &&
organization.PlanType == PlanType &&
organization.Seats == Seats &&
organization.MaxCollections == MaxCollections &&
organization.UseGroups == UseGroups &&
organization.UseDirectory == UseDirectory &&
organization.UseTotp == UseTotp &&
organization.SelfHost == SelfHost;
}
else
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
}
public bool VerifySignature(X509Certificate2 certificate)
{
using(var rsa = certificate.GetRSAPublicKey())
{
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
}
}

View File

@ -0,0 +1,90 @@
using Bit.Core.Models.Table;
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Bit.Core.Models.Business
{
public class UserLicense : ILicense
{
public UserLicense()
{ }
public UserLicense(User user)
{
LicenseKey = "";
Id = user.Id;
Email = user.Email;
Version = 1;
}
public string LicenseKey { get; set; }
public Guid Id { get; set; }
public string Email { get; set; }
public bool Premium { get; set; }
public short? MaxStorageGb { get; set; }
public int Version { get; set; }
public DateTime Issued { get; set; }
public DateTime Expires { get; set; }
public bool Trial { get; set; }
public string Signature { get; set; }
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
public byte[] GetSignatureData()
{
string data = null;
if(Version == 1)
{
data = string.Format("user:{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}",
Version,
Utilities.CoreHelpers.ToEpocMilliseconds(Issued),
Utilities.CoreHelpers.ToEpocMilliseconds(Expires),
LicenseKey,
Id,
Email,
Premium,
MaxStorageGb);
}
else
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
return Encoding.UTF8.GetBytes(data);
}
public bool VerifyData(User user)
{
if(Issued > DateTime.UtcNow)
{
return false;
}
if(Expires < DateTime.UtcNow)
{
return false;
}
if(Version == 1)
{
return
user.LicenseKey.Equals(LicenseKey, StringComparison.InvariantCultureIgnoreCase) &&
user.Premium == Premium &&
user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase);
}
else
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
}
public bool VerifySignature(X509Certificate2 certificate)
{
using(var rsa = certificate.GetRSAPublicKey())
{
return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
}
}

View File

@ -19,12 +19,14 @@ namespace Bit.Core.Models.Table
public bool UseGroups { get; set; }
public bool UseDirectory { get; set; }
public bool UseTotp { get; set; }
public bool SelfHost { get; set; }
public long? Storage { get; set; }
public short? MaxStorageGb { get; set; }
public GatewayType? Gateway { get; set; }
public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public bool Enabled { get; set; } = true;
public string LicenseKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;

View File

@ -35,6 +35,7 @@ namespace Bit.Core.Models.Table
public GatewayType? Gateway { get; set; }
public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public string LicenseKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;

View File

@ -0,0 +1,10 @@
using Bit.Core.Models.Table;
namespace Bit.Core.Services
{
public interface ILicenseVerificationService
{
bool VerifyOrganizationPlan(Organization organization);
bool VerifyUserPremium(User user);
}
}

View File

@ -0,0 +1,111 @@
using Bit.Core.Models.Business;
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Bit.Core.Services
{
public class RsaLicenseVerificationService : ILicenseVerificationService
{
private readonly X509Certificate2 _certificate;
private readonly GlobalSettings _globalSettings;
private IDictionary<string, UserLicense> _userLicenseCache;
private IDictionary<string, OrganizationLicense> _organizationLicenseCache;
public RsaLicenseVerificationService(
IHostingEnvironment environment,
GlobalSettings globalSettings)
{
if(!environment.IsDevelopment() && !globalSettings.SelfHosted)
{
throw new Exception($"{nameof(RsaLicenseVerificationService)} can only be used for self hosted instances.");
}
_globalSettings = globalSettings;
_certificate = CoreHelpers.GetCertificate("licensing.crt", null);
if(false && !_certificate.Thumbprint.Equals(""))
{
throw new Exception("Invalid licensing certificate.");
}
if(!CoreHelpers.SettingHasValue(_globalSettings.LicenseDirectory))
{
throw new InvalidOperationException("No license directory.");
}
}
public bool VerifyOrganizationPlan(Organization organization)
{
if(_globalSettings.SelfHosted && !organization.SelfHost)
{
return false;
}
var license = ReadOrganiztionLicense(organization);
return license != null && license.VerifyData(organization) && license.VerifySignature(_certificate);
}
public bool VerifyUserPremium(User user)
{
if(!user.Premium)
{
return false;
}
var license = ReadUserLicense(user);
return license != null && license.VerifyData(user) && license.VerifySignature(_certificate);
}
private UserLicense ReadUserLicense(User user)
{
if(_userLicenseCache != null && _userLicenseCache.ContainsKey(user.LicenseKey))
{
return _userLicenseCache[user.LicenseKey];
}
var filePath = $"{_globalSettings.LicenseDirectory}/user/{user.LicenseKey}.json";
if(!File.Exists(filePath))
{
return null;
}
var data = File.ReadAllText(filePath, Encoding.UTF8);
var obj = JsonConvert.DeserializeObject<UserLicense>(data);
if(_userLicenseCache == null)
{
_userLicenseCache = new Dictionary<string, UserLicense>();
}
_userLicenseCache.Add(obj.LicenseKey, obj);
return obj;
}
private OrganizationLicense ReadOrganiztionLicense(Organization organization)
{
if(_organizationLicenseCache != null && _organizationLicenseCache.ContainsKey(organization.LicenseKey))
{
return _organizationLicenseCache[organization.LicenseKey];
}
var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.LicenseKey}.json";
if(!File.Exists(filePath))
{
return null;
}
var data = File.ReadAllText(filePath, Encoding.UTF8);
var obj = JsonConvert.DeserializeObject<OrganizationLicense>(data);
if(_organizationLicenseCache == null)
{
_organizationLicenseCache = new Dictionary<string, OrganizationLicense>();
}
_organizationLicenseCache.Add(obj.LicenseKey, obj);
return obj;
}
}
}

View File

@ -0,0 +1,29 @@
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Hosting;
using System;
namespace Bit.Core.Services
{
public class NoopLicenseVerificationService : ILicenseVerificationService
{
public NoopLicenseVerificationService(
IHostingEnvironment environment,
GlobalSettings globalSettings)
{
if(!environment.IsDevelopment() && globalSettings.SelfHosted)
{
throw new Exception($"{nameof(NoopLicenseVerificationService)} cannot be used for self hosted instances.");
}
}
public bool VerifyOrganizationPlan(Organization organization)
{
return true;
}
public bool VerifyUserPremium(User user)
{
return user.Premium;
}
}
}

View File

@ -107,6 +107,15 @@ 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)
@ -117,6 +126,7 @@ namespace Bit.Core.Utilities
services.AddSingleton<IBlockIpService, NoopBlockIpService>();
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
services.AddSingleton<ILicenseVerificationService, NoopLicenseVerificationService>();
}
public static IdentityBuilder AddCustomIdentityServices(

View File

@ -9,6 +9,14 @@
<DockerComposeProjectPath>..\..\docker\Docker.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<None Remove="licensing.cer" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="licensing.cer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>

View File

@ -4,7 +4,7 @@
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:33656/",
"sslPort": 44392
"sslPort": 0
}
},
"profiles": {

BIN
src/Identity/licensing.cer Normal file

Binary file not shown.

View File

@ -10,12 +10,14 @@
@UseGroups BIT,
@UseDirectory BIT,
@UseTotp BIT,
@SelfHost BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT,
@LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@ -35,12 +37,14 @@ BEGIN
[UseGroups],
[UseDirectory],
[UseTotp],
[SelfHost],
[Storage],
[MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[Enabled],
[LicenseKey],
[CreationDate],
[RevisionDate]
)
@ -57,12 +61,14 @@ BEGIN
@UseGroups,
@UseDirectory,
@UseTotp,
@SelfHost,
@Storage,
@MaxStorageGb,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@Enabled,
@LicenseKey,
@CreationDate,
@RevisionDate
)

View File

@ -10,12 +10,14 @@
@UseGroups BIT,
@UseDirectory BIT,
@UseTotp BIT,
@SelfHost BIT,
@Storage BIGINT,
@MaxStorageGb SMALLINT,
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT,
@LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
@ -36,12 +38,14 @@ BEGIN
[UseGroups] = @UseGroups,
[UseDirectory] = @UseDirectory,
[UseTotp] = @UseTotp,
[SelfHost] = @SelfHost,
[Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[Enabled] = @Enabled,
[LicenseKey] = @LicenseKey,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE

View File

@ -21,6 +21,7 @@
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@ -51,6 +52,7 @@ BEGIN
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[LicenseKey],
[CreationDate],
[RevisionDate]
)
@ -78,6 +80,7 @@ BEGIN
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@LicenseKey,
@CreationDate,
@RevisionDate
)

View File

@ -21,6 +21,7 @@
@Gateway TINYINT,
@GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@LicenseKey VARCHAR(100),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
@ -51,6 +52,7 @@ BEGIN
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[LicenseKey] = @LicenseKey,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE

View File

@ -10,12 +10,14 @@
[UseGroups] BIT NOT NULL,
[UseDirectory] BIT NOT NULL,
[UseTotp] BIT NOT NULL,
[SelfHost] BIT NOT NULL,
[Storage] BIGINT NULL,
[MaxStorageGb] SMALLINT NULL,
[Gateway] TINYINT NULL,
[GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
[Enabled] BIT NOT NULL,
[LicenseKey] VARCHAR (100) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)

View File

@ -21,6 +21,7 @@
[Gateway] TINYINT NULL,
[GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
[LicenseKey] VARCHAR (100) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)

View File

@ -260,6 +260,7 @@ globalSettings:attachment:baseDirectory={_outputDir}/core/attachments
globalSettings:attachment:baseUrl={_url}/attachments
globalSettings:dataProtection:directory={_outputDir}/core/aspnet-dataprotection
globalSettings:logDirectory={_outputDir}/core/logs
globalSettings:licenseDirectory={_outputDir}/core/licenses
globalSettings:duo:aKey={Helpers.SecureRandomString(32, alpha: true, numeric: true)}
globalSettings:yubico:clientId=REPLACE
globalSettings:yubico:REPLACE");

View File

@ -0,0 +1,26 @@
alter table [Organization] add [SelfHost] BIT NULL
go
update [Organization] set [SelfHost] = 0
go
update [Organization] set [SelfHost] = 1 where PlanType = 4 or PlanType = 5
go
alter table [Organization] alter column [SelfHost] BIT NOT NULL
go
drop view [dbo].[OrganizationView]
go
CREATE VIEW [dbo].[OrganizationView]
AS
SELECT
*
FROM
[dbo].[Organization]
GO