1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-28 14:54:50 -05:00

update libs and cleanup

This commit is contained in:
Kyle Spearrin 2017-07-14 09:05:15 -04:00
parent 5786be651e
commit 5a4bfe4e61
24 changed files with 32 additions and 806 deletions

View File

@ -28,7 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="1.1.2" />
<PackageReference Include="AspNetCoreRateLimit" Version="1.0.5" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.1" />
<PackageReference Include="System.Net.Http" Version="4.3.2" />
</ItemGroup>

View File

@ -278,7 +278,7 @@ namespace Bit.Api.Controllers
if(userId.HasValue)
{
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
revisionDate = Core.Utilities.CoreHelpers.ToEpocMilliseconds(date);
revisionDate = CoreHelpers.ToEpocMilliseconds(date);
}
return revisionDate;

View File

@ -2,7 +2,6 @@
"globalSettings": {
"siteName": "bitwarden",
"baseVaultUri": "http://localhost:4001/#",
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
"stripeApiKey": "SECRET",
"sqlServer": {
"connectionString": "SECRET"
@ -11,13 +10,6 @@
"apiKey": "SECRET",
"replyToEmail": "hello@bitwarden.com"
},
"push": {
"apnsCertificateThumbprint": "SECRET",
"apnsCertificatePassword": "SECRET",
"gcmSenderId": "SECRET",
"gcmApiKey": "SECRET",
"gcmAppPackageName": "com.x8bit.bitwarden"
},
"identityServer": {
"certificateThumbprint": "SECRET"
},

View File

@ -2,7 +2,6 @@
"globalSettings": {
"siteName": "bitwarden",
"baseVaultUri": "http://localhost:4001/#",
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
"stripeApiKey": "SECRET",
"sqlServer": {
"connectionString": "SECRET"
@ -11,13 +10,6 @@
"apiKey": "SECRET",
"replyToEmail": "hello@bitwarden.com"
},
"push": {
"apnsCertificateThumbprint": "SECRET",
"apnsCertificatePassword": "SECRET",
"gcmSenderId": "SECRET",
"gcmApiKey": "SECRET",
"gcmAppPackageName": "com.x8bit.bitwarden"
},
"identityServer": {
"certificateThumbprint": "SECRET"
},

View File

@ -52,16 +52,15 @@
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="1.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="1.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.2" />
<PackageReference Include="RazorLight" Version="1.1.0" />
<PackageReference Include="Sendgrid" Version="9.2.0" />
<PackageReference Include="PushSharp" Version="4.0.10" />
<PackageReference Include="Sendgrid" Version="9.5.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.AzureDocumentDB" Version="3.6.1" />
<PackageReference Include="Stripe.net" Version="7.8.0" />
<PackageReference Include="U2F.Core" Version="1.0.3" />
<PackageReference Include="WindowsAzure.Storage" Version="8.1.1" />
<PackageReference Include="WindowsAzure.Storage" Version="8.1.4" />
<PackageReference Include="Otp.NET" Version="1.0.1" />
<PackageReference Include="YubicoDotNetClient" Version="1.0.0" />
</ItemGroup>

View File

@ -4,11 +4,9 @@
{
public virtual string SiteName { get; set; }
public virtual string BaseVaultUri { get; set; }
public virtual string JwtSigningKey { get; set; }
public virtual string StripeApiKey { get; set; }
public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings();
public virtual PushSettings Push { get; set; } = new PushSettings();
public virtual StorageSettings Storage { get; set; } = new StorageSettings();
public virtual AttachmentSettings Attachment { get; set; } = new AttachmentSettings();
public virtual IdentityServerSettings IdentityServer { get; set; } = new IdentityServerSettings();
@ -51,15 +49,6 @@
}
}
public class PushSettings
{
public string ApnsCertificateThumbprint { get; set; }
public string ApnsCertificatePassword { get; set; }
public string GcmSenderId { get; set; }
public string GcmApiKey { get; set; }
public string GcmAppPackageName { get; set; }
}
public class IdentityServerSettings
{
public string CertificateThumbprint { get; set; }

View File

@ -1,10 +0,0 @@
using System;
namespace Bit.Core
{
public interface IDataObject<T> where T : IEquatable<T>
{
T Id { get; set; }
void SetNewId();
}
}

View File

@ -1,104 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Bit.Core.Models.Table;
using Bit.Core.Enums;
using Bit.Core.Utilities.Duo;
using System.Collections.Generic;
using System.Net.Http;
namespace Bit.Core.Identity
{
public class DuoTokenProvider : IUserTwoFactorTokenProvider<User>
{
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{
if(!user.Premium)
{
return Task.FromResult(false);
}
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
var canGenerate = user.TwoFactorProviderIsEnabled(TwoFactorProviderType.Duo)
&& !string.IsNullOrWhiteSpace((string)provider?.MetaData["UserId"]);
return Task.FromResult(canGenerate);
}
/// <param name="purpose">Ex: "auto", "push", "passcode:123456", "sms", "phone"</param>
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
{
if(!user.Premium)
{
return null;
}
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
var duoClient = new DuoApi((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"],
(string)provider.MetaData["Host"]);
var parts = purpose.Split(':');
var parameters = new Dictionary<string, string>
{
["async"] = "1",
["user_id"] = (string)provider.MetaData["UserId"],
["factor"] = parts[0]
};
if(parameters["factor"] == "passcode" && parts.Length > 1)
{
parameters["passcode"] = parts[1];
}
else
{
parameters["device"] = "auto";
}
try
{
var response = await duoClient.JSONApiCallAsync<Dictionary<string, object>>(HttpMethod.Post,
"/auth/v2/auth", parameters);
if(response.ContainsKey("txid"))
{
var txId = response["txid"] as string;
return txId;
}
}
catch(DuoException) { }
return null;
}
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
{
if(!user.Premium)
{
return false;
}
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
var duoClient = new DuoApi((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"],
(string)provider.MetaData["Host"]);
var parameters = new Dictionary<string, string>
{
["txid"] = token
};
try
{
var response = await duoClient.JSONApiCallAsync<Dictionary<string, object>>(HttpMethod.Get,
"/auth/v2/auth_status", parameters);
var result = response["result"] as string;
return string.Equals(result, "allow");
}
catch(DuoException)
{
// TODO: We might want to return true in some cases? What if Duo is down?
}
return false;
}
}
}

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace Bit.Core.Models.Table
{
public class Cipher : IDataObject<Guid>
public class Cipher : ITableObject<Guid>
{
private Dictionary<string, CipherAttachment.MetaData> _attachmentData;

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
{
public class Collection : IDataObject<Guid>
public class Collection : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
{
public class Device : IDataObject<Guid>
public class Device : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid UserId { get; set; }

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
{
public class Folder : IDataObject<Guid>
public class Folder : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid UserId { get; set; }

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
{
public class Group : IDataObject<Guid>
public class Group : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }

View File

@ -0,0 +1,10 @@
using System;
namespace Bit.Core.Models.Table
{
public interface ITableObject<T> where T : IEquatable<T>
{
T Id { get; set; }
void SetNewId();
}
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
namespace Bit.Core.Models.Table
{
public class Organization : IDataObject<Guid>, ISubscriber, IStorable, IStorableSubscriber, IRevisable
public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorableSubscriber, IRevisable
{
public Guid Id { get; set; }
public string Name { get; set; }

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
namespace Bit.Core.Models.Table
{
public class OrganizationUser : IDataObject<Guid>
public class OrganizationUser : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }

View File

@ -2,7 +2,7 @@
namespace Bit.Core.Models.Table
{
public class U2f : IDataObject<int>
public class U2f : ITableObject<int>
{
public int Id { get; set; }
public Guid UserId { get; set; }

View File

@ -7,7 +7,7 @@ using System.Linq;
namespace Bit.Core.Models.Table
{
public class User : IDataObject<Guid>, ISubscriber, IStorable, IStorableSubscriber, IRevisable
public class User : ITableObject<Guid>, ISubscriber, IStorable, IStorableSubscriber, IRevisable
{
private Dictionary<TwoFactorProviderType, TwoFactorProvider> _twoFactorProviders;

View File

@ -1,9 +1,10 @@
using System;
using Bit.Core.Models.Table;
using System;
using System.Threading.Tasks;
namespace Bit.Core.Repositories
{
public interface IRepository<T, TId> where TId : IEquatable<TId> where T : class, IDataObject<TId>
public interface IRepository<T, TId> where TId : IEquatable<TId> where T : class, ITableObject<TId>
{
Task<T> GetByIdAsync(TId id);
Task CreateAsync(T obj);

View File

@ -4,10 +4,13 @@ using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Bit.Core.Models.Table;
namespace Bit.Core.Repositories.SqlServer
{
public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId> where TId : IEquatable<TId> where T : class, IDataObject<TId>
public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
where TId : IEquatable<TId>
where T : class, ITableObject<TId>
{
public Repository(string connectionString, string schema = null, string table = null)
: base(connectionString)

View File

@ -1,352 +0,0 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Repositories;
using Newtonsoft.Json.Linq;
using PushSharp.Google;
using PushSharp.Apple;
using Microsoft.AspNetCore.Hosting;
using PushSharp.Core;
using Bit.Core.Models.Table;
using Bit.Core.Enums;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Http;
using Bit.Core.Models;
namespace Bit.Core.Services
{
[Obsolete]
public class PushSharpPushNotificationService : IPushNotificationService
{
private readonly IDeviceRepository _deviceRepository;
private readonly ILogger<IPushNotificationService> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private GcmServiceBroker _gcmBroker;
private ApnsServiceBroker _apnsBroker;
public PushSharpPushNotificationService(
IDeviceRepository deviceRepository,
IHttpContextAccessor httpContextAccessor,
ILogger<IPushNotificationService> logger,
IHostingEnvironment hostingEnvironment,
GlobalSettings globalSettings)
{
_deviceRepository = deviceRepository;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
InitGcmBroker(globalSettings);
InitApnsBroker(globalSettings, hostingEnvironment);
}
public async Task PushSyncCipherCreateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
switch(cipher.Type)
{
case CipherType.Login:
await PushCipherAsync(cipher, PushType.SyncLoginDelete);
break;
default:
break;
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushCipherAsync(Cipher cipher, PushType type)
{
if(!cipher.UserId.HasValue)
{
// No push for org ciphers at the moment.
return;
}
var message = new SyncCipherPushNotification
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
RevisionDate = cipher.RevisionDate
};
var excludedTokens = new List<string>();
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
excludedTokens.Add(currentContext.DeviceIdentifier);
}
await PushToAllUserDevicesAsync(cipher.UserId.Value, type, message, excludedTokens);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
var excludedTokens = new List<string>();
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
excludedTokens.Add(currentContext.DeviceIdentifier);
}
await PushToAllUserDevicesAsync(folder.UserId, type, message, excludedTokens);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncSettings);
}
private async Task PushSyncUserAsync(Guid userId, PushType type)
{
var message = new SyncUserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
await PushToAllUserDevicesAsync(userId, type, message, null);
}
private void InitGcmBroker(GlobalSettings globalSettings)
{
if(string.IsNullOrWhiteSpace(globalSettings.Push.GcmSenderId) || string.IsNullOrWhiteSpace(globalSettings.Push.GcmApiKey)
|| string.IsNullOrWhiteSpace(globalSettings.Push.GcmAppPackageName))
{
return;
}
var gcmConfig = new GcmConfiguration(globalSettings.Push.GcmSenderId, globalSettings.Push.GcmApiKey,
globalSettings.Push.GcmAppPackageName);
_gcmBroker = new GcmServiceBroker(gcmConfig);
_gcmBroker.OnNotificationFailed += GcmBroker_OnNotificationFailed;
_gcmBroker.OnNotificationSucceeded += (notification) =>
{
Debug.WriteLine("GCM Notification Sent!");
};
_gcmBroker.Start();
}
private void GcmBroker_OnNotificationFailed(GcmNotification notification, AggregateException exception)
{
exception.Handle(ex =>
{
// See what kind of exception it was to further diagnose
if(ex is GcmNotificationException)
{
var notificationException = ex as GcmNotificationException;
// Deal with the failed notification
var gcmNotification = notificationException.Notification;
var description = notificationException.Description;
Debug.WriteLine($"GCM Notification Failed: ID={gcmNotification.MessageId}, Desc={description}");
}
else if(ex is GcmMulticastResultException)
{
var multicastException = ex as GcmMulticastResultException;
foreach(var succeededNotification in multicastException.Succeeded)
{
Debug.WriteLine($"GCM Notification Failed: ID={succeededNotification.MessageId}");
}
foreach(var failedKvp in multicastException.Failed)
{
var n = failedKvp.Key;
var e = failedKvp.Value;
Debug.WriteLine($"GCM Notification Failed: ID={n.MessageId}, Desc={e.Message}");
}
}
else if(ex is DeviceSubscriptionExpiredException)
{
var expiredException = ex as DeviceSubscriptionExpiredException;
var oldId = expiredException.OldSubscriptionId;
var newId = expiredException.NewSubscriptionId;
Debug.WriteLine($"Device RegistrationId Expired: {oldId}");
if(!string.IsNullOrWhiteSpace(newId))
{
// If this value isn't null, our subscription changed and we should update our database
Debug.WriteLine($"Device RegistrationId Changed To: {newId}");
}
}
else if(ex is RetryAfterException)
{
var retryException = (RetryAfterException)ex;
// If you get rate limited, you should stop sending messages until after the RetryAfterUtc date
Debug.WriteLine($"GCM Rate Limited, don't send more until after {retryException.RetryAfterUtc}");
}
else
{
Debug.WriteLine("GCM Notification Failed for some unknown reason");
}
// Mark it as handled
return true;
});
}
private void InitApnsBroker(GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
if(string.IsNullOrWhiteSpace(globalSettings.Push.ApnsCertificatePassword)
|| string.IsNullOrWhiteSpace(globalSettings.Push.ApnsCertificateThumbprint))
{
return;
}
var apnsCertificate = CoreHelpers.GetCertificate(globalSettings.Push.ApnsCertificateThumbprint);
if(apnsCertificate == null)
{
return;
}
var apnsConfig = new ApnsConfiguration(hostingEnvironment.IsProduction() ?
ApnsConfiguration.ApnsServerEnvironment.Production : ApnsConfiguration.ApnsServerEnvironment.Sandbox,
apnsCertificate.RawData, globalSettings.Push.ApnsCertificatePassword);
_apnsBroker = new ApnsServiceBroker(apnsConfig);
_apnsBroker.OnNotificationFailed += ApnsBroker_OnNotificationFailed;
_apnsBroker.OnNotificationSucceeded += (notification) =>
{
Debug.WriteLine("Apple Notification Sent!");
};
_apnsBroker.Start();
var feedbackService = new FeedbackService(apnsConfig);
feedbackService.FeedbackReceived += FeedbackService_FeedbackReceived;
feedbackService.Check();
}
private void ApnsBroker_OnNotificationFailed(ApnsNotification notification, AggregateException exception)
{
exception.Handle(ex =>
{
// See what kind of exception it was to further diagnose
if(ex is ApnsNotificationException)
{
var notificationException = ex as ApnsNotificationException;
// Deal with the failed notification
var apnsNotification = notificationException.Notification;
var statusCode = notificationException.ErrorStatusCode;
Debug.WriteLine($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");
}
else
{
// Inner exception might hold more useful information like an ApnsConnectionException
Debug.WriteLine($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
}
// Mark it as handled
return true;
});
}
private void FeedbackService_FeedbackReceived(string deviceToken, DateTime timestamp)
{
// Remove the deviceToken from your database
// timestamp is the time the token was reported as expired
}
private async Task PushToAllUserDevicesAsync(Guid userId, PushType type, object message, IEnumerable<string> tokensToSkip)
{
var devices = (await _deviceRepository.GetManyByUserIdAsync(userId))
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken) && (!tokensToSkip?.Contains(d.PushToken) ?? true));
if(devices.Count() == 0)
{
return;
}
if(_apnsBroker != null)
{
var appleNotification = new ApplePayloadPushNotification
{
Data = new PayloadPushNotification.DataObj(type, JsonConvert.SerializeObject(message))
};
var obj = JObject.FromObject(appleNotification);
// Send to each iOS device
foreach(var device in devices.Where(d => d.Type == DeviceType.iOS))
{
_apnsBroker.QueueNotification(new ApnsNotification
{
DeviceToken = device.PushToken,
Payload = obj
});
}
}
// Android can send to many devices at once
var androidDevices = devices.Where(d => d.Type == DeviceType.Android);
if(_gcmBroker != null && androidDevices.Count() > 0)
{
var gcmData = new PayloadPushNotification.DataObj(type, JsonConvert.SerializeObject(message));
var obj = JObject.FromObject(gcmData);
_gcmBroker.QueueNotification(new GcmNotification
{
RegistrationIds = androidDevices.Select(d => d.PushToken).ToList(),
Data = obj
});
}
}
}
}

View File

@ -5,7 +5,6 @@ using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@ -16,7 +15,7 @@ namespace Bit.Core.Utilities
public static class CoreHelpers
{
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
private static readonly DateTime _epoc = new DateTime(1970, 1, 1);
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Generate sequential Guid for Sql Server.

View File

@ -1,285 +0,0 @@
/*
Original source modified from https://github.com/duosecurity/duo_api_csharp
=============================================================================
=============================================================================
ref: https://github.com/duosecurity/duo_api_csharp/blob/master/LICENSE
Copyright (c) 2013, Duo Security, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Text;
using System.Globalization;
using Newtonsoft.Json;
using System.Net.Http;
using System.Threading.Tasks;
namespace Bit.Core.Utilities.Duo
{
public class DuoApi
{
public const string DefaultAgent = "Duo.NET, bitwarden";
private readonly string _ikey;
private readonly string _skey;
private readonly string _host;
private readonly string _userAgent;
public DuoApi(string ikey, string skey, string host)
: this(ikey, skey, host, null)
{ }
protected DuoApi(string ikey, string skey, string host, string userAgent)
{
_ikey = ikey;
_skey = skey;
_host = host;
_userAgent = string.IsNullOrWhiteSpace(userAgent) ? DefaultAgent : userAgent;
}
public async Task<Tuple<string, HttpStatusCode>> ApiCallAsync(HttpMethod method, string path,
Dictionary<string, string> parameters, int? timeout = null, DateTime? date = null)
{
var canonParams = CanonicalizeParams(parameters);
var query = string.Empty;
if(method != HttpMethod.Post && method != HttpMethod.Put && parameters.Count > 0)
{
query = "?" + canonParams;
}
var url = $"https://{_host}{path}{query}";
var dateString = DateToRFC822(date.GetValueOrDefault(DateTime.UtcNow));
var auth = Sign(method.ToString(), path, canonParams, dateString);
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Authorization", auth);
client.DefaultRequestHeaders.Add("X-Duo-Date", dateString);
client.DefaultRequestHeaders.Add("User-Agent", _userAgent);
if(timeout.GetValueOrDefault(0) > 0)
{
client.Timeout = new TimeSpan(0, 0, 0, 0, timeout.Value);
}
var request = new HttpRequestMessage
{
RequestUri = new Uri(url),
Method = method
};
if(method == HttpMethod.Post || method == HttpMethod.Put)
{
request.Content = new FormUrlEncodedContent(parameters);
}
HttpResponseMessage response = null;
try
{
response = await client.SendAsync(request);
}
catch(WebException)
{
if(response?.Content == null)
{
throw;
}
}
var result = await response.Content.ReadAsStringAsync();
return new Tuple<string, HttpStatusCode>(result, response.StatusCode);
}
public async Task<T> JSONApiCallAsync<T>(HttpMethod method, string path, Dictionary<string, string> parameters,
int? timeout = null, DateTime? date = null) where T : class
{
var resTuple = await ApiCallAsync(method, path, parameters, timeout, date);
var res = resTuple.Item1;
HttpStatusCode statusCode = resTuple.Item2;
try
{
var resDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(res);
var stat = resDict["stat"] as string;
if(stat == "OK")
{
return JsonConvert.DeserializeObject<T>(resDict["response"].ToString());
}
else
{
var code = resDict["code"] as int?;
var message = resDict["message"] as string;
var messageDetail = string.Empty;
if(resDict.ContainsKey("message_detail"))
{
messageDetail = resDict["message_detail"] as string;
}
throw new DuoApiException(code.GetValueOrDefault(0), statusCode, message, messageDetail);
}
}
catch(Exception e)
{
throw new DuoBadResponseException(statusCode, e);
}
}
private string CanonicalizeParams(Dictionary<string, string> parameters)
{
var ret = new List<string>();
foreach(var pair in parameters)
{
var p = $"{WebUtility.UrlEncode(pair.Key)}={WebUtility.UrlEncode(pair.Value)}";
// Signatures require upper-case hex digits.
p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant());
// Escape only the expected characters.
p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X"));
p = p.Replace("%7E", "~");
// UrlEncode converts space (" ") to "+". The
// signature algorithm requires "%20" instead. Actual
// + has already been replaced with %2B.
p = p.Replace("+", "%20");
ret.Add(p);
}
ret.Sort(StringComparer.Ordinal);
return string.Join("&", ret.ToArray());
}
private string CanonicalizeRequest(string method, string path, string canon_params, string date)
{
string[] lines = { date, method.ToUpperInvariant(), _host.ToLower(), path, canon_params };
return string.Join("\n", lines);
}
private string Sign(string method, string path, string canon_params, string date)
{
var canon = CanonicalizeRequest(method, path, canon_params, date);
var sig = HmacSign(canon);
var auth = $"{_ikey }:{sig}";
var authBytes = Encoding.ASCII.GetBytes(auth);
return $"Basic {Convert.ToBase64String(authBytes)}";
}
private string HmacSign(string data)
{
var keyBytes = Encoding.ASCII.GetBytes(_skey);
var dataBytes = Encoding.ASCII.GetBytes(data);
using(var hmac = new HMACSHA1(keyBytes))
{
var hash = hmac.ComputeHash(dataBytes);
var hex = BitConverter.ToString(hash);
return hex.Replace("-", "").ToLower();
}
}
private string DateToRFC822(DateTime date)
{
// Can't use the "zzzz" format because it adds a ":"
// between the offset's hours and minutes.
var dateString = date.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
// TODO: Get proper timezone offset. hardcoded to UTC for now.
var offset = 0;
string zone;
// + or -, then 0-pad, then offset, then more 0-padding.
if(offset < 0)
{
offset *= -1;
zone = "-";
}
else
{
zone = "+";
}
zone += offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
dateString += (" " + zone.PadRight(5, '0'));
return dateString;
}
}
public class DuoException : Exception
{
public HttpStatusCode Status { get; private set; }
public DuoException(HttpStatusCode status, string message, Exception inner)
: base(message, inner)
{
Status = status;
}
}
public class DuoApiException : DuoException
{
public int Code { get; private set; }
public string ApiMessage { get; private set; }
public string ApiMessageDetail { get; private set; }
public DuoApiException(int code, HttpStatusCode status, string message, string messageDetail)
: base(status, FormatMessage(code, message, messageDetail), null)
{
Code = code;
ApiMessage = message;
ApiMessageDetail = messageDetail;
}
private static string FormatMessage(int code, string message, string messageDetail)
{
return $"Duo API Error {code}: '{message}' ('{messageDetail}').";
}
}
public class DuoBadResponseException : DuoException
{
public DuoBadResponseException(HttpStatusCode status, Exception inner)
: base(status, FormatMessage(status, inner), inner)
{ }
private static string FormatMessage(HttpStatusCode status, Exception inner)
{
var innerMessage = "(null)";
if(inner != null)
{
innerMessage = string.Format("'{0}'", inner.Message);
}
return $"Got error '{innerMessage}' with HTTP status {(int)status}.";
}
}
}

View File

@ -2,7 +2,6 @@
"globalSettings": {
"siteName": "bitwarden",
"baseVaultUri": "http://localhost:4001/#",
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
"stripeApiKey": "SECRET",
"sqlServer": {
"connectionString": "SECRET"
@ -11,13 +10,6 @@
"apiKey": "SECRET",
"replyToEmail": "hello@bitwarden.com"
},
"push": {
"apnsCertificateThumbprint": "SECRET",
"apnsCertificatePassword": "SECRET",
"gcmSenderId": "SECRET",
"gcmApiKey": "SECRET",
"gcmAppPackageName": "com.x8bit.bitwarden"
},
"identityServer": {
"certificateThumbprint": "SECRET"
},