1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-29 13:25:17 +01:00
bitwarden-server/src/Core/Utilities/CoreHelpers.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

979 lines
36 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Data;
2017-12-15 21:23:57 +01:00
using System.Globalization;
2017-08-11 23:06:31 +02:00
using System.IO;
using System.Linq;
2017-08-11 23:06:31 +02:00
using System.Reflection;
2017-06-29 21:55:39 +02:00
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
2017-06-29 21:55:39 +02:00
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
2017-12-15 21:23:57 +01:00
using System.Threading.Tasks;
2018-03-22 18:18:18 +01:00
using System.Web;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
2018-12-20 04:27:45 +01:00
using Azure.Storage.Queues.Models;
using Bit.Core.Context;
2019-07-11 02:05:07 +02:00
using Bit.Core.Enums;
2020-01-10 14:33:13 +01:00
using Bit.Core.Enums.Provider;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Settings;
2021-12-16 15:35:09 +01:00
using Dapper;
using IdentityModel;
2021-06-30 09:35:26 +02:00
using Microsoft.AspNetCore.DataProtection;
using MimeKit;
2017-07-10 20:30:12 +02:00
using Newtonsoft.Json;
namespace Bit.Core.Utilities
{
public static class CoreHelpers
{
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
2017-07-14 15:05:15 +02:00
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
2017-12-15 21:23:57 +01:00
private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly Random _random = new Random();
2017-08-25 14:57:43 +02:00
private static string _version;
2017-09-12 05:08:08 +02:00
private static readonly string _qwertyDvorakMap = "-=qwertyuiop[]asdfghjkl;'zxcvbnm,./_+QWERTYUIO" +
"P{}ASDFGHJKL:\"ZXCVBNM<>?";
private static readonly string _dvorakMap = "[]',.pyfgcrl/=aoeuidhtns-;qjkxbmwvz{}\"<>PYFGC" +
"RL?+AOEUIDHTNS_:QJKXBMWVZ";
2017-09-12 05:25:11 +02:00
private static readonly string _qwertyColemakMap = "qwertyuiopasdfghjkl;zxcvbnmQWERTYUIOPASDFGHJKL:ZXCVBNM";
private static readonly string _colemakMap = "qwfpgjluy;arstdhneiozxcvbkmQWFPGJLUY:ARSTDHNEIOZXCVBKM";
2019-09-03 20:08:08 +02:00
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
private static readonly string RealIp = "X-Real-IP";
/// <summary>
/// Generate sequential Guid for Sql Server.
/// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs
/// </summary>
/// <returns>A comb Guid.</returns>
public static Guid GenerateComb()
{
var guidArray = Guid.NewGuid().ToByteArray();
var now = DateTime.UtcNow;
// Get the days and milliseconds which will be used to build the byte string
var days = new TimeSpan(now.Ticks - _baseDateTicks);
var msecs = now.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
var daysArray = BitConverter.GetBytes(days.Days);
var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);
return new Guid(guidArray);
}
2019-07-25 21:50:13 +02:00
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
T[] bucket = null;
var count = 0;
foreach (var item in source)
2019-07-25 21:50:13 +02:00
{
if (bucket == null)
2019-07-25 21:50:13 +02:00
{
bucket = new T[size];
}
bucket[count++] = item;
if (count != size)
2019-07-25 21:50:13 +02:00
{
continue;
}
yield return bucket.Select(x => x);
bucket = null;
count = 0;
}
// Return the last bucket with all remaining elements
if (bucket != null && count > 0)
2019-07-25 21:50:13 +02:00
{
yield return bucket.Take(count);
}
}
public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)
{
return ids.ToArrayTVP("GuidId");
}
public static DataTable ToArrayTVP<T>(this IEnumerable<T> values, string columnName)
{
2017-08-09 14:14:45 +02:00
var table = new DataTable();
2017-08-22 18:38:48 +02:00
table.SetTypeName($"[dbo].[{columnName}Array]");
table.Columns.Add(columnName, typeof(T));
if (values != null)
{
foreach (var value in values)
{
table.Rows.Add(value);
}
}
return table;
}
public static DataTable ToArrayTVP(this IEnumerable<SelectionReadOnly> values)
{
2017-08-09 14:14:45 +02:00
var table = new DataTable();
2017-08-22 18:38:48 +02:00
table.SetTypeName("[dbo].[SelectionReadOnlyArray]");
var idColumn = new DataColumn("Id", typeof(Guid));
table.Columns.Add(idColumn);
var readOnlyColumn = new DataColumn("ReadOnly", typeof(bool));
table.Columns.Add(readOnlyColumn);
var hidePasswordsColumn = new DataColumn("HidePasswords", typeof(bool));
table.Columns.Add(hidePasswordsColumn);
if (values != null)
{
foreach (var value in values)
{
var row = table.NewRow();
row[idColumn] = value.Id;
row[readOnlyColumn] = value.ReadOnly;
row[hidePasswordsColumn] = value.HidePasswords;
table.Rows.Add(row);
}
}
return table;
}
Support large organization sync (#1311) * Increase organization max seat size from 30k to 2b (#1274) * Increase organization max seat size from 30k to 2b * PR review. Do not modify unless state matches expected * Organization sync simultaneous event reporting (#1275) * Split up azure messages according to max size * Allow simultaneous login of organization user events * Early resolve small event lists * Clarify logic Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Improve readability This comes at the cost of multiple serializations, but the improvement in wire-time should more than make up for this on message where serialization time matters Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Queue emails (#1286) * Extract common Azure queue methods * Do not use internal entity framework namespace * Prefer IEnumerable to IList unless needed All of these implementations were just using `Count == 1`, which is easily replicated. This will be used when abstracting Azure queues * Add model for azure queue message * Abstract Azure queue for reuse * Creat service to enqueue mail messages for later processing Azure queue mail service uses Azure queues. Blocking just blocks until all the work is done -- This is how emailing works today * Provide mail queue service to DI * Queue organization invite emails for later processing All emails can later be added to this queue * Create Admin hosted service to process enqueued mail messages * Prefer constructors to static generators * Mass delete organization users (#1287) * Add delete many to Organization Users * Correct formatting * Remove erroneous migration * Clarify parameter name * Formatting fixes * Simplify bump account revision sproc * Formatting fixes * Match file names to objects * Indicate if large import is expected * Early pull all existing users we were planning on inviting (#1290) * Early pull all existing users we were planning on inviting * Improve sproc name * Batch upsert org users (#1289) * Add UpsertMany sprocs to OrganizationUser * Add method to create TVPs from any object. Uses DbOrder attribute to generate. Sproc will fail unless TVP column order matches that of the db type * Combine migrations * Correct formatting * Include sql objects in sql project * Keep consisten parameter names * Batch deletes for performance * Correct formatting * consolidate migrations * Use batch methods in OrganizationImport * Declare @BatchSize * Transaction names limited to 32 chars Drop sproc before creating it if it exists * Update import tests * Allow for more users in org upgrades * Fix formatting * Improve class hierarchy structure * Use name tuple types * Fix formatting * Front load all reflection * Format constructor * Simplify ToTvp as class-specific extension Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-05-17 16:43:02 +02:00
public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{
var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> getter)>
{
(nameof(OrganizationUser.Id), typeof(Guid), ou => ou.Id),
(nameof(OrganizationUser.OrganizationId), typeof(Guid), ou => ou.OrganizationId),
(nameof(OrganizationUser.UserId), typeof(Guid), ou => ou.UserId),
(nameof(OrganizationUser.Email), typeof(string), ou => ou.Email),
(nameof(OrganizationUser.Key), typeof(string), ou => ou.Key),
(nameof(OrganizationUser.Status), typeof(byte), ou => ou.Status),
(nameof(OrganizationUser.Type), typeof(byte), ou => ou.Type),
(nameof(OrganizationUser.AccessAll), typeof(bool), ou => ou.AccessAll),
(nameof(OrganizationUser.ExternalId), typeof(string), ou => ou.ExternalId),
(nameof(OrganizationUser.CreationDate), typeof(DateTime), ou => ou.CreationDate),
(nameof(OrganizationUser.RevisionDate), typeof(DateTime), ou => ou.RevisionDate),
(nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions),
(nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey),
Support large organization sync (#1311) * Increase organization max seat size from 30k to 2b (#1274) * Increase organization max seat size from 30k to 2b * PR review. Do not modify unless state matches expected * Organization sync simultaneous event reporting (#1275) * Split up azure messages according to max size * Allow simultaneous login of organization user events * Early resolve small event lists * Clarify logic Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Improve readability This comes at the cost of multiple serializations, but the improvement in wire-time should more than make up for this on message where serialization time matters Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Queue emails (#1286) * Extract common Azure queue methods * Do not use internal entity framework namespace * Prefer IEnumerable to IList unless needed All of these implementations were just using `Count == 1`, which is easily replicated. This will be used when abstracting Azure queues * Add model for azure queue message * Abstract Azure queue for reuse * Creat service to enqueue mail messages for later processing Azure queue mail service uses Azure queues. Blocking just blocks until all the work is done -- This is how emailing works today * Provide mail queue service to DI * Queue organization invite emails for later processing All emails can later be added to this queue * Create Admin hosted service to process enqueued mail messages * Prefer constructors to static generators * Mass delete organization users (#1287) * Add delete many to Organization Users * Correct formatting * Remove erroneous migration * Clarify parameter name * Formatting fixes * Simplify bump account revision sproc * Formatting fixes * Match file names to objects * Indicate if large import is expected * Early pull all existing users we were planning on inviting (#1290) * Early pull all existing users we were planning on inviting * Improve sproc name * Batch upsert org users (#1289) * Add UpsertMany sprocs to OrganizationUser * Add method to create TVPs from any object. Uses DbOrder attribute to generate. Sproc will fail unless TVP column order matches that of the db type * Combine migrations * Correct formatting * Include sql objects in sql project * Keep consisten parameter names * Batch deletes for performance * Correct formatting * consolidate migrations * Use batch methods in OrganizationImport * Declare @BatchSize * Transaction names limited to 32 chars Drop sproc before creating it if it exists * Update import tests * Allow for more users in org upgrades * Fix formatting * Improve class hierarchy structure * Use name tuple types * Fix formatting * Front load all reflection * Format constructor * Simplify ToTvp as class-specific extension Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-05-17 16:43:02 +02:00
};
foreach (var (name, type, getter) in columnData)
{
var column = new DataColumn(name, type);
table.Columns.Add(column);
}
foreach (var orgUser in orgUsers ?? new OrganizationUser[] { })
{
var row = table.NewRow();
foreach (var (name, type, getter) in columnData)
{
var val = getter(orgUser);
if (val == null)
{
row[name] = DBNull.Value;
}
else
{
row[name] = val;
}
}
table.Rows.Add(row);
}
return table;
}
2017-08-11 23:06:31 +02:00
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
2017-08-11 23:06:31 +02:00
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);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
if (certCollection.Count > 0)
{
cert = certCollection[0];
}
certStore.Close();
return cert;
}
2017-08-07 17:24:16 +02:00
public static X509Certificate2 GetCertificate(string file, string password)
{
return new X509Certificate2(file, password);
}
2020-01-13 21:35:50 +01:00
public async static Task<X509Certificate2> GetEmbeddedCertificateAsync(string file, string password)
2017-08-11 23:06:31 +02:00
{
var assembly = typeof(CoreHelpers).GetTypeInfo().Assembly;
using (var s = assembly.GetManifestResourceStream($"Bit.Core.{file}"))
using (var ms = new MemoryStream())
2017-08-11 23:06:31 +02:00
{
2020-01-13 21:35:50 +01:00
await s.CopyToAsync(ms);
2017-08-11 23:06:31 +02:00
return new X509Certificate2(ms.ToArray(), password);
}
}
public static string GetEmbeddedResourceContentsAsync(string file)
{
var assembly = Assembly.GetCallingAssembly();
var resourceName = assembly.GetManifestResourceNames().Single(n => n.EndsWith(file));
using (var stream = assembly.GetManifestResourceStream(resourceName))
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
public async static Task<X509Certificate2> GetBlobCertificateAsync(string connectionString, string container, string file, string password)
2019-07-11 02:05:07 +02:00
{
try
2019-07-11 02:05:07 +02:00
{
var blobServiceClient = new BlobServiceClient(connectionString);
var containerRef2 = blobServiceClient.GetBlobContainerClient(container);
var blobRef = containerRef2.GetBlobClient(file);
using var memStream = new MemoryStream();
await blobRef.DownloadToAsync(memStream).ConfigureAwait(false);
return new X509Certificate2(memStream.ToArray(), password);
}
catch (RequestFailedException ex)
when (ex.ErrorCode == BlobErrorCode.ContainerNotFound || ex.ErrorCode == BlobErrorCode.BlobNotFound)
{
return null;
}
catch (Exception)
{
return null;
2019-07-11 02:05:07 +02:00
}
}
public static long ToEpocMilliseconds(DateTime date)
{
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
}
public static DateTime FromEpocMilliseconds(long milliseconds)
{
return _epoc.AddMilliseconds(milliseconds);
}
2017-06-22 23:03:35 +02:00
2017-08-12 04:55:25 +02:00
public static long ToEpocSeconds(DateTime date)
{
return (long)Math.Round((date - _epoc).TotalSeconds, 0);
}
public static DateTime FromEpocSeconds(long seconds)
{
return _epoc.AddSeconds(seconds);
}
2017-06-22 23:03:35 +02:00
public static string U2fAppIdUrl(GlobalSettings globalSettings)
{
2017-08-04 05:12:05 +02:00
return string.Concat(globalSettings.BaseServiceUri.Vault, "/app-id.json");
2017-06-22 23:03:35 +02:00
}
2017-06-29 21:55:39 +02:00
public static string RandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
2017-06-29 21:55:39 +02:00
bool numeric = true, bool special = false)
{
return RandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
}
2017-06-29 21:55:39 +02:00
public static string RandomString(int length, string characters)
{
return new string(Enumerable.Repeat(characters, length).Select(s => s[_random.Next(s.Length)]).ToArray());
}
2017-06-29 21:55:39 +02:00
public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
bool numeric = true, bool special = false)
{
return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
2017-06-29 21:55:39 +02:00
}
// ref https://stackoverflow.com/a/8996788/1090359 with modifications
public static string SecureRandomString(int length, string characters)
{
if (length < 0)
2017-06-29 21:55:39 +02:00
{
throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero.");
}
if ((characters?.Length ?? 0) == 0)
2017-06-29 21:55:39 +02:00
{
throw new ArgumentOutOfRangeException(nameof(characters), "characters invalid.");
}
const int byteSize = 0x100;
if (byteSize < characters.Length)
2017-06-29 21:55:39 +02:00
{
throw new ArgumentException(
string.Format("{0} may contain no more than {1} characters.", nameof(characters), byteSize),
nameof(characters));
}
var outOfRangeStart = byteSize - (byteSize % characters.Length);
using (var rng = RandomNumberGenerator.Create())
2017-06-29 21:55:39 +02:00
{
var sb = new StringBuilder();
var buffer = new byte[128];
while (sb.Length < length)
2017-06-29 21:55:39 +02:00
{
rng.GetBytes(buffer);
for (var i = 0; i < buffer.Length && sb.Length < length; ++i)
2017-06-29 21:55:39 +02:00
{
// Divide the byte into charSet-sized groups. If the random value falls into the last group and the
// last group is too small to choose from the entire allowedCharSet, ignore the value in order to
// avoid biasing the result.
if (outOfRangeStart <= buffer[i])
2017-06-29 21:55:39 +02:00
{
continue;
}
sb.Append(characters[buffer[i] % characters.Length]);
}
}
return sb.ToString();
}
}
2017-07-01 05:01:41 +02:00
private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)
{
var characters = string.Empty;
if (alpha)
{
if (upper)
{
characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
if (lower)
{
characters += "abcdefghijklmnopqrstuvwxyz";
}
}
if (numeric)
{
characters += "0123456789";
}
if (special)
{
characters += "!@#$%^*&";
}
return characters;
}
2017-07-01 05:01:41 +02:00
// ref: https://stackoverflow.com/a/11124118/1090359
// Returns the human-readable file size for an arbitrary 64-bit file size .
// The format is "0.## XB", ex: "4.2 KB" or "1.43 GB"
public static string ReadableBytesSize(long size)
{
// Get absolute value
var absoluteSize = (size < 0 ? -size : size);
// Determine the suffix and readable value
string suffix;
double readable;
if (absoluteSize >= 0x40000000) // 1 Gigabyte
2017-07-01 05:01:41 +02:00
{
suffix = "GB";
readable = (size >> 20);
}
else if (absoluteSize >= 0x100000) // 1 Megabyte
2017-07-01 05:01:41 +02:00
{
suffix = "MB";
readable = (size >> 10);
}
else if (absoluteSize >= 0x400) // 1 Kilobyte
2017-07-01 05:01:41 +02:00
{
suffix = "KB";
readable = size;
}
else
{
return size.ToString("0 Bytes"); // Byte
2017-07-01 05:01:41 +02:00
}
// Divide by 1024 to get fractional value
readable = (readable / 1024);
// Return formatted number with suffix
return readable.ToString("0.## ") + suffix;
}
2017-07-10 20:30:12 +02:00
/// <summary>
/// Creates a clone of the given object through serializing to json and deserializing.
/// This method is subject to the limitations of Newstonsoft. For example, properties with
/// inaccessible setters will not be set.
/// </summary>
2017-07-10 20:30:12 +02:00
public static T CloneObject<T>(T obj)
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
}
public static bool SettingHasValue(string setting)
{
2018-08-16 00:43:26 +02:00
var normalizedSetting = setting?.ToLowerInvariant();
return !string.IsNullOrWhiteSpace(normalizedSetting) && !normalizedSetting.Equals("secret") &&
!normalizedSetting.Equals("replace");
}
public static string Base64EncodeString(string input)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(input));
}
public static string Base64DecodeString(string input)
{
return Encoding.UTF8.GetString(Convert.FromBase64String(input));
}
public static string Base64UrlEncodeString(string input)
{
return Base64UrlEncode(Encoding.UTF8.GetBytes(input));
}
public static string Base64UrlDecodeString(string input)
{
return Encoding.UTF8.GetString(Base64UrlDecode(input));
}
public static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input)
.Replace('+', '-')
.Replace('/', '_')
.Replace("=", string.Empty);
return output;
}
public static byte[] Base64UrlDecode(string input)
{
var output = input;
// 62nd char of encoding
output = output.Replace('-', '+');
// 63rd char of encoding
output = output.Replace('_', '/');
// Pad with trailing '='s
switch (output.Length % 4)
{
case 0:
// No pad chars in this case
break;
case 2:
// Two pad chars
output += "=="; break;
case 3:
// One pad char
output += "="; break;
default:
throw new InvalidOperationException("Illegal base64url string!");
}
// Standard base64 decoder
return Convert.FromBase64String(output);
}
2017-08-16 19:55:01 +02:00
public static string PunyEncode(string text)
{
if (text == "")
{
return "";
}
if (text == null)
{
return null;
}
if (!text.Contains("@"))
{
// Assume domain name or non-email address
var idn = new IdnMapping();
return idn.GetAscii(text);
}
else
{
// Assume email address
return MailboxAddress.EncodeAddrspec(text);
}
}
2017-08-16 19:55:01 +02:00
public static string FormatLicenseSignatureValue(object val)
{
if (val == null)
2017-08-16 19:55:01 +02:00
{
return string.Empty;
}
if (val.GetType() == typeof(DateTime))
2017-08-16 19:55:01 +02:00
{
return ToEpocSeconds((DateTime)val).ToString();
}
if (val.GetType() == typeof(bool))
2017-08-16 19:55:01 +02:00
{
return val.ToString().ToLowerInvariant();
}
if (val is PlanType planType)
{
return planType switch
{
PlanType.Free => "Free",
PlanType.FamiliesAnnually2019 => "FamiliesAnnually",
PlanType.TeamsMonthly2019 => "TeamsMonthly",
PlanType.TeamsAnnually2019 => "TeamsAnnually",
PlanType.EnterpriseMonthly2019 => "EnterpriseMonthly",
PlanType.EnterpriseAnnually2019 => "EnterpriseAnnually",
PlanType.Custom => "Custom",
_ => ((byte)planType).ToString(),
};
}
2017-08-16 19:55:01 +02:00
return val.ToString();
}
2017-08-25 14:57:43 +02:00
2017-09-07 05:57:14 +02:00
public static string GetVersion()
2017-08-25 14:57:43 +02:00
{
if (string.IsNullOrWhiteSpace(_version))
2017-08-25 14:57:43 +02:00
{
_version = Assembly.GetEntryAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
}
2017-09-07 05:57:14 +02:00
return _version;
2017-08-25 14:57:43 +02:00
}
2017-09-12 05:08:08 +02:00
public static string Dvorak2Qwerty(string value)
{
return Other2Qwerty(value, _dvorakMap, _qwertyDvorakMap);
}
2017-09-12 16:11:56 +02:00
public static string Colemak2Qwerty(string value)
2017-09-12 05:08:08 +02:00
{
2017-09-12 05:25:11 +02:00
return Other2Qwerty(value, _colemakMap, _qwertyColemakMap);
2017-09-12 05:08:08 +02:00
}
private static string Other2Qwerty(string value, string otherMap, string qwertyMap)
{
var sb = new StringBuilder();
foreach (var c in value)
2017-09-12 05:08:08 +02:00
{
sb.Append(otherMap.IndexOf(c) > -1 ? qwertyMap[otherMap.IndexOf(c)] : c);
}
return sb.ToString();
}
public static string SanitizeForEmail(string value, bool htmlEncode = true)
{
var cleanedValue = value.Replace("@", "[at]");
var regexOptions = RegexOptions.CultureInvariant |
RegexOptions.Singleline |
RegexOptions.IgnoreCase;
cleanedValue = Regex.Replace(cleanedValue, @"(\.\w)",
m => string.Concat("[dot]", m.ToString().Last()), regexOptions);
while (Regex.IsMatch(cleanedValue, @"((^|\b)(\w*)://)", regexOptions))
{
cleanedValue = Regex.Replace(cleanedValue, @"((^|\b)(\w*)://)",
string.Empty, regexOptions);
}
return htmlEncode ? HttpUtility.HtmlEncode(cleanedValue) : cleanedValue;
}
2017-11-29 04:21:47 +01:00
public static string DateTimeToTableStorageKey(DateTime? date = null)
{
if (date.HasValue)
2017-12-15 21:23:57 +01:00
{
date = date.Value.ToUniversalTime();
}
else
2017-11-29 04:21:47 +01:00
{
date = DateTime.UtcNow;
}
2017-12-15 21:23:57 +01:00
return _max.Subtract(date.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
2017-11-29 04:21:47 +01:00
}
2018-03-22 18:18:18 +01:00
// ref: https://stackoverflow.com/a/27545010/1090359
public static Uri ExtendQuery(Uri uri, IDictionary<string, string> values)
{
var baseUri = uri.ToString();
var queryString = string.Empty;
if (baseUri.Contains("?"))
2018-03-22 18:18:18 +01:00
{
var urlSplit = baseUri.Split('?');
baseUri = urlSplit[0];
queryString = urlSplit.Length > 1 ? urlSplit[1] : string.Empty;
}
var queryCollection = HttpUtility.ParseQueryString(queryString);
foreach (var kvp in values ?? new Dictionary<string, string>())
2018-03-22 18:18:18 +01:00
{
queryCollection[kvp.Key] = kvp.Value;
}
var uriKind = uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative;
if (queryCollection.Count == 0)
2018-03-22 18:18:18 +01:00
{
return new Uri(baseUri, uriKind);
}
return new Uri(string.Format("{0}?{1}", baseUri, queryCollection), uriKind);
}
2018-12-20 04:27:45 +01:00
public static string CustomProviderName(TwoFactorProviderType type)
{
return string.Concat("Custom_", type.ToString());
}
2019-06-11 23:17:23 +02:00
2020-02-28 15:15:47 +01:00
public static bool UserInviteTokenIsValid(IDataProtector protector, string token, string userEmail,
Guid orgUserId, IGlobalSettings globalSettings)
{
return TokenIsValid("OrganizationUserInvite", protector, token, userEmail, orgUserId,
globalSettings.OrganizationInviteExpirationHours);
}
public static bool TokenIsValid(string firstTokenPart, IDataProtector protector, string token, string userEmail,
Guid id, double expirationInHours)
2019-06-11 23:17:23 +02:00
{
var invalid = true;
try
{
var unprotectedData = protector.Unprotect(token);
var dataParts = unprotectedData.Split(' ');
if (dataParts.Length == 4 && dataParts[0] == firstTokenPart &&
new Guid(dataParts[1]) == id &&
2019-06-11 23:17:23 +02:00
dataParts[2].Equals(userEmail, StringComparison.InvariantCultureIgnoreCase))
{
var creationTime = FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
var expTime = creationTime.AddHours(expirationInHours);
2019-06-11 23:17:23 +02:00
invalid = expTime < DateTime.UtcNow;
}
}
catch
{
invalid = true;
}
return !invalid;
}
public static string GetApplicationCacheServiceBusSubcriptionName(GlobalSettings globalSettings)
{
var subName = globalSettings.ServiceBus.ApplicationCacheSubscriptionName;
if (string.IsNullOrWhiteSpace(subName))
{
var websiteInstanceId = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID");
if (string.IsNullOrWhiteSpace(websiteInstanceId))
{
throw new Exception("No service bus subscription name available.");
}
else
{
subName = $"{globalSettings.ProjectName.ToLower()}_{websiteInstanceId}";
if (subName.Length > 50)
{
subName = subName.Substring(0, 50);
}
}
}
return subName;
}
2019-09-03 20:08:08 +02:00
public static string GetIpAddress(this Microsoft.AspNetCore.Http.HttpContext httpContext,
GlobalSettings globalSettings)
{
if (httpContext == null)
2019-09-03 20:08:08 +02:00
{
return null;
}
if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(CloudFlareConnectingIp))
2019-09-03 20:08:08 +02:00
{
return httpContext.Request.Headers[CloudFlareConnectingIp].ToString();
}
if (globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(RealIp))
2019-09-03 20:08:08 +02:00
{
return httpContext.Request.Headers[RealIp].ToString();
}
return httpContext.Connection?.RemoteIpAddress?.ToString();
}
public static bool IsCorsOriginAllowed(string origin, GlobalSettings globalSettings)
{
return
// Web vault
origin == globalSettings.BaseServiceUri.Vault ||
// Safari extension origin
origin == "file://" ||
// Product website
(!globalSettings.SelfHosted && origin == "https://bitwarden.com");
}
public static X509Certificate2 GetIdentityServerCertificate(GlobalSettings globalSettings)
{
if (globalSettings.SelfHosted &&
SettingHasValue(globalSettings.IdentityServer.CertificatePassword)
&& File.Exists("identity.pfx"))
{
return GetCertificate("identity.pfx",
globalSettings.IdentityServer.CertificatePassword);
}
else if (SettingHasValue(globalSettings.IdentityServer.CertificateThumbprint))
{
return GetCertificate(
globalSettings.IdentityServer.CertificateThumbprint);
}
else if (!globalSettings.SelfHosted &&
SettingHasValue(globalSettings.Storage?.ConnectionString) &&
SettingHasValue(globalSettings.IdentityServer.CertificatePassword))
{
return GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
"identity.pfx", globalSettings.IdentityServer.CertificatePassword).GetAwaiter().GetResult();
}
return null;
}
public static Dictionary<string, object> AdjustIdentityServerConfig(Dictionary<string, object> configDict,
string publicServiceUri, string internalServiceUri)
{
var dictReplace = new Dictionary<string, object>();
foreach (var item in configDict)
{
if (item.Key == "authorization_endpoint" && item.Value is string val)
{
var uri = new Uri(val);
dictReplace.Add(item.Key, string.Concat(publicServiceUri, uri.LocalPath));
}
2020-09-01 18:28:03 +02:00
else if ((item.Key == "jwks_uri" || item.Key.EndsWith("_endpoint")) && item.Value is string val2)
{
2020-09-01 18:28:03 +02:00
var uri = new Uri(val2);
dictReplace.Add(item.Key, string.Concat(internalServiceUri, uri.LocalPath));
}
}
foreach (var replace in dictReplace)
{
configDict[replace.Key] = replace.Value;
}
return configDict;
}
2021-06-30 09:35:26 +02:00
public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContentOrganization> orgs,
ICollection<CurrentContentProvider> providers, bool isPremium)
{
var claims = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("premium", isPremium ? "true" : "false"),
new KeyValuePair<string, string>(JwtClaimTypes.Email, user.Email),
new KeyValuePair<string, string>(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
new KeyValuePair<string, string>("sstamp", user.SecurityStamp)
};
if (!string.IsNullOrWhiteSpace(user.Name))
{
claims.Add(new KeyValuePair<string, string>(JwtClaimTypes.Name, user.Name));
}
// Orgs that this user belongs to
if (orgs.Any())
{
foreach (var group in orgs.GroupBy(o => o.Type))
{
switch (group.Key)
{
case Enums.OrganizationUserType.Owner:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgowner", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.Admin:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgadmin", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.Manager:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgmanager", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.User:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orguser", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.Custom:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgcustom", org.Id.ToString()));
foreach (var (permission, claimName) in org.Permissions.ClaimsMap)
{
if (!permission)
{
continue;
}
claims.Add(new KeyValuePair<string, string>(claimName, org.Id.ToString()));
}
}
break;
default:
break;
}
}
}
2021-12-16 15:35:09 +01:00
2021-06-30 09:35:26 +02:00
if (providers.Any())
{
foreach (var group in providers.GroupBy(o => o.Type))
{
switch (group.Key)
{
case ProviderUserType.ProviderAdmin:
foreach (var provider in group)
{
claims.Add(new KeyValuePair<string, string>("providerprovideradmin", provider.Id.ToString()));
}
break;
case ProviderUserType.ServiceUser:
foreach (var provider in group)
{
claims.Add(new KeyValuePair<string, string>("providerserviceuser", provider.Id.ToString()));
}
break;
}
}
}
2021-12-16 15:35:09 +01:00
return claims;
}
public static T LoadClassFromJsonData<T>(string jsonData) where T : new()
{
if (string.IsNullOrWhiteSpace(jsonData))
{
return new T();
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
}
public static string ClassToJsonData<T>(T data)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Serialize(data, options);
}
public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
{
if (list.Contains(item))
{
return list;
}
list.Add(item);
return list;
}
public static string DecodeMessageText(this QueueMessage message)
{
var text = message?.MessageText;
if (string.IsNullOrWhiteSpace(text))
{
return text;
}
try
{
return Base64DecodeString(text);
}
catch
{
return text;
}
}
public static bool FixedTimeEquals(string input1, string input2)
{
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(input1), Encoding.UTF8.GetBytes(input2));
}
Families for Enterprise (#1714) * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Fix build error * Update emails * Fix tests * Skip local test * Add newline * Fix stripe subscription update * Finish emails * Skip test * Fix unit tests * Remove unused variable * Fix unit tests * Switch to handlebars ifs * Remove ending email * Remove reconfirmation template * Switch naming convention * Switch naming convention * Fix migration * Update copy and links * Switch to using Guid in the method * Remove unneeded css styles * Add sql files to Sql.sqlproj * Removed old comments * Made name more verbose * Fix SQL error * Move unit tests to service * Fix sp * Revert "Move unit tests to service" This reverts commit 1185bf3ec8ca36ccd75717ed2463adf8885159a6. * Do repository validation in service layer * Fix tests * Fix merge conflicts and remove TODO * Remove unneeded models * Fix spacing and formatting * Switch Org -> Organization * Remove single use variables * Switch method name * Fix Controller * Switch to obfuscating email * Fix unit tests Co-authored-by: Justin Baur <admin@justinbaur.com>
2021-11-19 23:25:06 +01:00
public static string ObfuscateEmail(string email)
{
if (email == null)
{
return email;
}
var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);
if (emailParts.Length != 2)
{
return email;
}
var username = emailParts[0];
if (username.Length < 2)
{
return email;
}
var sb = new StringBuilder();
sb.Append(emailParts[0][..2]);
for (var i = 2; i < emailParts[0].Length; i++)
{
sb.Append('*');
}
return sb.Append('@')
.Append(emailParts[1])
.ToString();
}
}
}