mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
log in and store protected settings
This commit is contained in:
@@ -15,6 +15,11 @@ namespace Bit.Console
|
||||
private static string[] _args = null;
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
MainAsync(args).Wait();
|
||||
}
|
||||
|
||||
private static async Task MainAsync(string[] args)
|
||||
{
|
||||
_args = args;
|
||||
_usingArgs = args.Length > 0;
|
||||
@@ -22,6 +27,8 @@ namespace Bit.Console
|
||||
|
||||
while(true)
|
||||
{
|
||||
Con.ResetColor();
|
||||
|
||||
if(_usingArgs)
|
||||
{
|
||||
selection = args[0];
|
||||
@@ -45,10 +52,12 @@ namespace Bit.Console
|
||||
{
|
||||
case "1":
|
||||
case "login":
|
||||
LogIn();
|
||||
case "signin":
|
||||
await LogInAsync();
|
||||
break;
|
||||
case "2":
|
||||
case "dir":
|
||||
case "directory":
|
||||
|
||||
break;
|
||||
case "3":
|
||||
@@ -57,6 +66,7 @@ namespace Bit.Console
|
||||
break;
|
||||
case "4":
|
||||
case "svc":
|
||||
case "service":
|
||||
|
||||
break;
|
||||
case "5":
|
||||
@@ -84,37 +94,60 @@ namespace Bit.Console
|
||||
_args = null;
|
||||
}
|
||||
|
||||
public static void LogIn()
|
||||
private static async Task LogInAsync()
|
||||
{
|
||||
string email = null;
|
||||
SecureString masterPassword = null;
|
||||
string masterPassword = null;
|
||||
|
||||
if(_usingArgs)
|
||||
{
|
||||
email = _args[1];
|
||||
masterPassword = new SecureString();
|
||||
foreach(var c in _args[2])
|
||||
{
|
||||
masterPassword.AppendChar(c);
|
||||
}
|
||||
Con.ForegroundColor = ConsoleColor.Red;
|
||||
Con.WriteLine("You cannot log in via arguments. Use the console instead.");
|
||||
Con.ResetColor();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Con.Write("Email: ");
|
||||
email = Con.ReadLine();
|
||||
email = Con.ReadLine().Trim();
|
||||
Con.Write("Master password: ");
|
||||
masterPassword = ReadSecureLine();
|
||||
}
|
||||
|
||||
// TODO: Do login
|
||||
var result = await Core.Services.AuthService.Instance.LogInAsync(email, masterPassword);
|
||||
|
||||
if(result.TwoFactorRequired)
|
||||
{
|
||||
Con.WriteLine();
|
||||
Con.WriteLine();
|
||||
Con.WriteLine("Two-step login is enabled on this account. Please enter your verification code.");
|
||||
Con.Write("Verification code: ");
|
||||
var token = Con.ReadLine().Trim();
|
||||
result = await Core.Services.AuthService.Instance.LogInTwoFactorAsync(token, email, result.MasterPasswordHash);
|
||||
}
|
||||
|
||||
Con.WriteLine();
|
||||
Con.WriteLine();
|
||||
if(result.Success)
|
||||
{
|
||||
Con.ForegroundColor = ConsoleColor.Green;
|
||||
Con.WriteLine("You have successfully logged in as {0}!", Core.Services.TokenService.Instance.AccessTokenEmail);
|
||||
Con.ResetColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
Con.ForegroundColor = ConsoleColor.Red;
|
||||
Con.WriteLine(result.ErrorMessage);
|
||||
Con.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
public static SecureString ReadSecureLine()
|
||||
private static string ReadSecureLine()
|
||||
{
|
||||
var input = new SecureString();
|
||||
var input = string.Empty;
|
||||
while(true)
|
||||
{
|
||||
ConsoleKeyInfo i = Con.ReadKey(true);
|
||||
var i = Con.ReadKey(true);
|
||||
if(i.Key == ConsoleKey.Enter)
|
||||
{
|
||||
break;
|
||||
@@ -123,13 +156,13 @@ namespace Bit.Console
|
||||
{
|
||||
if(input.Length > 0)
|
||||
{
|
||||
input.RemoveAt(input.Length - 1);
|
||||
input = input.Remove(input.Length - 1);
|
||||
Con.Write("\b \b");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
input.AppendChar(i.KeyChar);
|
||||
input = string.Concat(input, i.KeyChar);
|
||||
Con.Write("*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core
|
||||
{
|
||||
public class AuthService
|
||||
{
|
||||
private static AuthService _instance;
|
||||
|
||||
private AuthService() { }
|
||||
|
||||
public static AuthService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new AuthService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Authenticated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,12 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="BouncyCastle.Crypto, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
|
||||
<HintPath>..\..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.DirectoryServices" />
|
||||
@@ -43,9 +49,25 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="TokenService.cs" />
|
||||
<Compile Include="AuthService.cs" />
|
||||
<Compile Include="Models\ApiError.cs" />
|
||||
<Compile Include="Models\ApiResult.cs" />
|
||||
<Compile Include="Models\LoginResult.cs" />
|
||||
<Compile Include="Models\ErrorResponse.cs" />
|
||||
<Compile Include="Models\EncryptedData.cs" />
|
||||
<Compile Include="Models\TokenRequest.cs" />
|
||||
<Compile Include="Models\TokenResponse.cs" />
|
||||
<Compile Include="Services\ApiService.cs" />
|
||||
<Compile Include="Services\SettingsService.cs" />
|
||||
<Compile Include="Services\CryptoService.cs" />
|
||||
<Compile Include="Services\TokenService.cs" />
|
||||
<Compile Include="Services\AuthService.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Utilities\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
13
src/Core/Models/ApiError.cs
Normal file
13
src/Core/Models/ApiError.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ApiError
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
75
src/Core/Models/ApiResult.cs
Normal file
75
src/Core/Models/ApiResult.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ApiResult<T>
|
||||
{
|
||||
private List<ApiError> m_errors = new List<ApiError>();
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
public T Result { get; set; }
|
||||
public IEnumerable<ApiError> Errors => m_errors;
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
|
||||
public static ApiResult<T> Success(T result, HttpStatusCode statusCode)
|
||||
{
|
||||
return new ApiResult<T>
|
||||
{
|
||||
Succeeded = true,
|
||||
Result = result,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
public static ApiResult<T> Failed(HttpStatusCode statusCode, params ApiError[] errors)
|
||||
{
|
||||
var result = new ApiResult<T>
|
||||
{
|
||||
Succeeded = false,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
|
||||
if(errors != null)
|
||||
{
|
||||
result.m_errors.AddRange(errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiResult
|
||||
{
|
||||
private List<ApiError> m_errors = new List<ApiError>();
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
public IEnumerable<ApiError> Errors => m_errors;
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
|
||||
public static ApiResult Success(HttpStatusCode statusCode)
|
||||
{
|
||||
return new ApiResult
|
||||
{
|
||||
Succeeded = true,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
public static ApiResult Failed(HttpStatusCode statusCode, params ApiError[] errors)
|
||||
{
|
||||
var result = new ApiResult
|
||||
{
|
||||
Succeeded = false,
|
||||
StatusCode = statusCode
|
||||
};
|
||||
|
||||
if(errors != null)
|
||||
{
|
||||
result.m_errors.AddRange(errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Core/Models/EncryptedData.cs
Normal file
35
src/Core/Models/EncryptedData.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class EncryptedData
|
||||
{
|
||||
public EncryptedData() { }
|
||||
|
||||
public EncryptedData(byte[] plainValue)
|
||||
{
|
||||
IV = RandomBytes();
|
||||
Value = ProtectedData.Protect(plainValue, IV, DataProtectionScope.CurrentUser);
|
||||
}
|
||||
|
||||
public byte[] Value { get; set; }
|
||||
public byte[] IV { get; set; }
|
||||
|
||||
public byte[] Decrypt()
|
||||
{
|
||||
return ProtectedData.Unprotect(Value, IV, DataProtectionScope.CurrentUser);
|
||||
}
|
||||
|
||||
private byte[] RandomBytes()
|
||||
{
|
||||
var entropy = new byte[16];
|
||||
new RNGCryptoServiceProvider().GetBytes(entropy);
|
||||
return entropy;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Core/Models/ErrorResponse.cs
Normal file
18
src/Core/Models/ErrorResponse.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class ErrorResponse
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }
|
||||
// For use in development environments.
|
||||
public string ExceptionMessage { get; set; }
|
||||
public string ExceptionStackTrace { get; set; }
|
||||
public string InnerExceptionMessage { get; set; }
|
||||
}
|
||||
}
|
||||
16
src/Core/Models/LoginResult.cs
Normal file
16
src/Core/Models/LoginResult.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class LoginResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public bool TwoFactorRequired { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
||||
33
src/Core/Models/TokenRequest.cs
Normal file
33
src/Core/Models/TokenRequest.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class TokenRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Token { get; set; }
|
||||
public int? Provider { get; set; }
|
||||
|
||||
public IDictionary<string, string> ToIdentityTokenRequest()
|
||||
{
|
||||
var dict = new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "password" },
|
||||
{ "username", Email },
|
||||
{ "password", MasterPasswordHash },
|
||||
{ "scope", "api offline_access" },
|
||||
{ "client_id", "mobile" }
|
||||
};
|
||||
|
||||
if(Token != null && Provider.HasValue)
|
||||
{
|
||||
dict.Add("TwoFactorToken", Token);
|
||||
dict.Add("TwoFactorProvider", Provider.Value.ToString());
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Core/Models/TokenResponse.cs
Normal file
23
src/Core/Models/TokenResponse.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Models
|
||||
{
|
||||
public class TokenResponse
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
[JsonProperty("expires_in")]
|
||||
public long ExpiresIn { get; set; }
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
public List<int> TwoFactorProviders { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
}
|
||||
}
|
||||
227
src/Core/Services/ApiService.cs
Normal file
227
src/Core/Services/ApiService.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class ApiService
|
||||
{
|
||||
private static ApiService _instance;
|
||||
|
||||
private ApiService()
|
||||
{
|
||||
ApiClient = new HttpClient();
|
||||
ApiClient.BaseAddress = new Uri("https://api.bitwarden.com");
|
||||
|
||||
IdentityClient = new HttpClient();
|
||||
IdentityClient.BaseAddress = new Uri("https://identity.bitwarden.com");
|
||||
}
|
||||
|
||||
public static ApiService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new ApiService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpClient ApiClient { get; private set; }
|
||||
protected HttpClient IdentityClient { get; private set; }
|
||||
|
||||
public virtual async Task<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj)
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(IdentityClient.BaseAddress, "connect/token"),
|
||||
Content = new FormUrlEncodedContent(requestObj.ToIdentityTokenRequest())
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await IdentityClient.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorResponse = JObject.Parse(responseContent);
|
||||
if(errorResponse["TwoFactorProviders"] != null)
|
||||
{
|
||||
return ApiResult<TokenResponse>.Success(new TokenResponse
|
||||
{
|
||||
TwoFactorProviders = errorResponse["TwoFactorProviders"].ToObject<List<int>>()
|
||||
}, response.StatusCode);
|
||||
}
|
||||
|
||||
return await HandleErrorAsync<TokenResponse>(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return HandledWebException<TokenResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
protected ApiResult HandledWebException()
|
||||
{
|
||||
return ApiResult.Failed(HttpStatusCode.BadGateway,
|
||||
new ApiError { Message = "There is a problem connecting to the server." });
|
||||
}
|
||||
|
||||
protected ApiResult<T> HandledWebException<T>()
|
||||
{
|
||||
return ApiResult<T>.Failed(HttpStatusCode.BadGateway,
|
||||
new ApiError { Message = "There is a problem connecting to the server." });
|
||||
}
|
||||
|
||||
protected async Task<ApiResult> HandleTokenStateAsync()
|
||||
{
|
||||
return await HandleTokenStateAsync(
|
||||
() => ApiResult.Success(HttpStatusCode.OK),
|
||||
() => HandledWebException(),
|
||||
(r) => HandleErrorAsync(r));
|
||||
}
|
||||
|
||||
protected async Task<ApiResult<T>> HandleTokenStateAsync<T>()
|
||||
{
|
||||
return await HandleTokenStateAsync(
|
||||
() => ApiResult<T>.Success(default(T), HttpStatusCode.OK),
|
||||
() => HandledWebException<T>(),
|
||||
(r) => HandleErrorAsync<T>(r));
|
||||
}
|
||||
|
||||
private async Task<T> HandleTokenStateAsync<T>(Func<T> success, Func<T> webException,
|
||||
Func<HttpResponseMessage, Task<T>> error)
|
||||
{
|
||||
if(TokenService.Instance.AccessTokenNeedsRefresh && !string.IsNullOrWhiteSpace(TokenService.Instance.RefreshToken))
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(IdentityClient.BaseAddress, "connect/token"),
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "client_id", "mobile" },
|
||||
{ "refresh_token", TokenService.Instance.RefreshToken }
|
||||
})
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await IdentityClient.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
if(response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
response.StatusCode = HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
return await error.Invoke(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
TokenService.Instance.AccessToken = tokenResponse.AccessToken;
|
||||
TokenService.Instance.RefreshToken = tokenResponse.RefreshToken;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return webException.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
return success.Invoke();
|
||||
}
|
||||
|
||||
protected async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
|
||||
return ApiResult<T>.Failed(response.StatusCode, errors.ToArray());
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
return ApiResult<T>.Failed(response.StatusCode,
|
||||
new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
protected async Task<ApiResult> HandleErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
|
||||
return ApiResult.Failed(response.StatusCode, errors.ToArray());
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
return ApiResult.Failed(response.StatusCode,
|
||||
new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
private async Task<List<ApiError>> ParseErrorsAsync(HttpResponseMessage response)
|
||||
{
|
||||
var errors = new List<ApiError>();
|
||||
var statusCode = (int)response.StatusCode;
|
||||
if(statusCode >= 400 && statusCode <= 500)
|
||||
{
|
||||
ErrorResponse errorResponseModel = null;
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
if(!string.IsNullOrWhiteSpace(responseContent))
|
||||
{
|
||||
var errorResponse = JObject.Parse(responseContent);
|
||||
if(errorResponse["ErrorModel"] != null && errorResponse["ErrorModel"]["Message"] != null)
|
||||
{
|
||||
errorResponseModel = errorResponse["ErrorModel"].ToObject<ErrorResponse>();
|
||||
}
|
||||
else if(errorResponse["Message"] != null)
|
||||
{
|
||||
errorResponseModel = errorResponse.ToObject<ErrorResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
if(errorResponseModel != null)
|
||||
{
|
||||
if((errorResponseModel.ValidationErrors?.Count ?? 0) > 0)
|
||||
{
|
||||
foreach(var valError in errorResponseModel.ValidationErrors)
|
||||
{
|
||||
foreach(var errorMessage in valError.Value)
|
||||
{
|
||||
errors.Add(new ApiError { Message = errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new ApiError { Message = errorResponseModel.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(errors.Count == 0)
|
||||
{
|
||||
errors.Add(new ApiError { Message = "An unknown error has occurred." });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/Core/Services/AuthService.cs
Normal file
100
src/Core/Services/AuthService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AuthService
|
||||
{
|
||||
private static AuthService _instance;
|
||||
|
||||
private AuthService() { }
|
||||
|
||||
public static AuthService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new AuthService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Authenticated => !string.IsNullOrWhiteSpace(TokenService.Instance.AccessToken);
|
||||
|
||||
public async Task<LoginResult> LogInAsync(string email, string masterPassword)
|
||||
{
|
||||
var normalizedEmail = email.Trim().ToLower();
|
||||
var key = CryptoService.Instance.MakeKeyFromPassword(masterPassword, normalizedEmail);
|
||||
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = normalizedEmail,
|
||||
MasterPasswordHash = CryptoService.Instance.HashPasswordBase64(key, masterPassword)
|
||||
};
|
||||
|
||||
var response = await ApiService.Instance.PostTokenAsync(request);
|
||||
|
||||
masterPassword = null;
|
||||
key = null;
|
||||
|
||||
var result = new LoginResult();
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
if(response.Result.TwoFactorProviders != null && response.Result.TwoFactorProviders.Count > 0)
|
||||
{
|
||||
result.TwoFactorRequired = true;
|
||||
result.MasterPasswordHash = request.MasterPasswordHash;
|
||||
return result;
|
||||
}
|
||||
|
||||
await ProcessLogInSuccessAsync(response.Result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<LoginResult> LogInTwoFactorAsync(string token, string email, string masterPasswordHash)
|
||||
{
|
||||
var request = new TokenRequest
|
||||
{
|
||||
Email = email.Trim().ToLower(),
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Token = token.Trim().Replace(" ", ""),
|
||||
Provider = 0 // Authenticator app (only 1 provider for now, so hard coded)
|
||||
};
|
||||
|
||||
var response = await ApiService.Instance.PostTokenAsync(request);
|
||||
|
||||
var result = new LoginResult();
|
||||
if(!response.Succeeded)
|
||||
{
|
||||
result.Success = false;
|
||||
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
await ProcessLogInSuccessAsync(response.Result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Task ProcessLogInSuccessAsync(TokenResponse response)
|
||||
{
|
||||
TokenService.Instance.AccessToken = response.AccessToken;
|
||||
TokenService.Instance.RefreshToken = response.RefreshToken;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/Core/Services/CryptoService.cs
Normal file
101
src/Core/Services/CryptoService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class CryptoService
|
||||
{
|
||||
private static CryptoService _instance;
|
||||
|
||||
private CryptoService() { }
|
||||
|
||||
public static CryptoService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new CryptoService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] MakeKeyFromPassword(string password, string salt)
|
||||
{
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
if(salt == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(salt));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var saltBytes = Encoding.UTF8.GetBytes(salt);
|
||||
|
||||
var keyBytes = DeriveKey(passwordBytes, saltBytes, 5000);
|
||||
|
||||
password = null;
|
||||
passwordBytes = null;
|
||||
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
public string MakeKeyFromPasswordBase64(string password, string salt)
|
||||
{
|
||||
var key = MakeKeyFromPassword(password, salt);
|
||||
password = null;
|
||||
return Convert.ToBase64String(key);
|
||||
}
|
||||
|
||||
public byte[] HashPassword(byte[] key, string password)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
|
||||
var hashBytes = DeriveKey(key, passwordBytes, 1);
|
||||
|
||||
password = null;
|
||||
key = null;
|
||||
|
||||
return hashBytes;
|
||||
}
|
||||
|
||||
public string HashPasswordBase64(byte[] key, string password)
|
||||
{
|
||||
var hash = HashPassword(key, password);
|
||||
password = null;
|
||||
key = null;
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
private byte[] DeriveKey(byte[] password, byte[] salt, int rounds)
|
||||
{
|
||||
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
|
||||
generator.Init(password, salt, rounds);
|
||||
var key = ((KeyParameter)generator.GenerateDerivedMacParameters(256)).GetKey();
|
||||
password = null;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/Core/Services/SettingsService.cs
Normal file
148
src/Core/Services/SettingsService.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class SettingsService
|
||||
{
|
||||
private static SettingsService _instance;
|
||||
private static object _locker = new object();
|
||||
private static string _baseStoragePath = string.Concat(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"\\bitwarden\\DirectoryConnector");
|
||||
|
||||
private IDictionary<string, object> _settings;
|
||||
|
||||
private SettingsService() { }
|
||||
|
||||
public static SettingsService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new SettingsService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
var filePath = $"{_baseStoragePath}\\settings.json";
|
||||
if(_settings == null && File.Exists(filePath))
|
||||
{
|
||||
var serializer = new JsonSerializer();
|
||||
using(var s = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using(var sr = new StreamReader(s, Encoding.UTF8))
|
||||
using(var jsonTextReader = new JsonTextReader(sr))
|
||||
{
|
||||
_settings = serializer.Deserialize<IDictionary<string, object>>(jsonTextReader);
|
||||
}
|
||||
}
|
||||
|
||||
return _settings == null ? new Dictionary<string, object>() : _settings;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock(_locker)
|
||||
{
|
||||
if(!Directory.Exists(_baseStoragePath))
|
||||
{
|
||||
Directory.CreateDirectory(_baseStoragePath);
|
||||
}
|
||||
|
||||
_settings = value;
|
||||
var filePath = $"{_baseStoragePath}\\settings.json";
|
||||
using(var s = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
using(var sw = new StreamWriter(s, Encoding.UTF8))
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_settings);
|
||||
sw.Write(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Set(string key, object value)
|
||||
{
|
||||
if(Contains(key))
|
||||
{
|
||||
Settings[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.Add(key, value);
|
||||
}
|
||||
|
||||
Settings = Settings;
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
Settings.Remove(key);
|
||||
Settings = Settings;
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return Settings.ContainsKey(key);
|
||||
}
|
||||
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
if(Settings.ContainsKey(key))
|
||||
{
|
||||
return (T)Settings[key];
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
public EncryptedData AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<EncryptedData>("AccessToken");
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
Remove("AccessTolen");
|
||||
return;
|
||||
}
|
||||
|
||||
Set("AccessToken", value);
|
||||
}
|
||||
}
|
||||
|
||||
public EncryptedData RefreshToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return Get<EncryptedData>("RefreshToken");
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
Remove("RefreshToken");
|
||||
return;
|
||||
}
|
||||
|
||||
Set("RefreshToken", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/Core/Services/TokenService.cs
Normal file
171
src/Core/Services/TokenService.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using Bit.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class TokenService
|
||||
{
|
||||
private static TokenService _instance;
|
||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private string _accessToken;
|
||||
private dynamic _decodedAccessToken;
|
||||
|
||||
private TokenService() { }
|
||||
|
||||
public static TokenService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new TokenService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public string AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_accessToken != null)
|
||||
{
|
||||
return _accessToken;
|
||||
}
|
||||
|
||||
var encBytes = SettingsService.Instance.AccessToken;
|
||||
if(encBytes != null)
|
||||
{
|
||||
_accessToken = Encoding.ASCII.GetString(encBytes.Decrypt());
|
||||
}
|
||||
|
||||
return _accessToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
_accessToken = value;
|
||||
if(_accessToken == null)
|
||||
{
|
||||
SettingsService.Instance.AccessToken = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(_accessToken);
|
||||
SettingsService.Instance.AccessToken = new EncryptedData(bytes);
|
||||
bytes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime AccessTokenExpiration
|
||||
{
|
||||
get
|
||||
{
|
||||
var decoded = DecodeAccessToken();
|
||||
if(decoded?["exp"] == null)
|
||||
{
|
||||
throw new InvalidOperationException("No exp in token.");
|
||||
}
|
||||
|
||||
return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>()));
|
||||
}
|
||||
}
|
||||
|
||||
public bool AccessTokenExpired => DateTime.UtcNow < AccessTokenExpiration;
|
||||
public TimeSpan AccessTokenTimeRemaining => AccessTokenExpiration - DateTime.UtcNow;
|
||||
public bool AccessTokenNeedsRefresh => AccessTokenTimeRemaining.TotalMinutes < 5;
|
||||
public string AccessTokenUserId => DecodeAccessToken()?["sub"].Value<string>();
|
||||
public string AccessTokenEmail => DecodeAccessToken()?["email"].Value<string>();
|
||||
public string AccessTokenName => DecodeAccessToken()?["name"].Value<string>();
|
||||
|
||||
public string RefreshToken
|
||||
{
|
||||
get
|
||||
{
|
||||
var encData = SettingsService.Instance.RefreshToken;
|
||||
if(encData != null)
|
||||
{
|
||||
return Encoding.ASCII.GetString(encData.Decrypt());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
SettingsService.Instance.RefreshToken = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(value);
|
||||
SettingsService.Instance.RefreshToken = new EncryptedData(bytes);
|
||||
bytes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JObject DecodeAccessToken()
|
||||
{
|
||||
if(_decodedAccessToken != null)
|
||||
{
|
||||
return _decodedAccessToken;
|
||||
}
|
||||
|
||||
if(AccessToken == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} not found.");
|
||||
}
|
||||
|
||||
var parts = AccessToken.Split('.');
|
||||
if(parts.Length != 3)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
|
||||
}
|
||||
|
||||
var decodedBytes = Base64UrlDecode(parts[1]);
|
||||
if(decodedBytes == null || decodedBytes.Length < 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
|
||||
}
|
||||
|
||||
_decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length));
|
||||
return _decodedAccessToken;
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core
|
||||
{
|
||||
public class TokenService
|
||||
{
|
||||
private static TokenService _instance;
|
||||
|
||||
private TokenService() { }
|
||||
|
||||
public static TokenService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new TokenService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public string AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
// TODO: get bytes from disc
|
||||
var encBytes = new byte[0];
|
||||
var bytes = ProtectedData.Unprotect(encBytes, RandomEntropy(), DataProtectionScope.CurrentUser);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
set
|
||||
{
|
||||
var bytes = Convert.FromBase64String(value);
|
||||
var encBytes = ProtectedData.Protect(bytes, RandomEntropy(), DataProtectionScope.CurrentUser);
|
||||
// TODO: store bytes to disc
|
||||
}
|
||||
}
|
||||
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
private byte[] RandomEntropy()
|
||||
{
|
||||
var entropy = new byte[16];
|
||||
new RNGCryptoServiceProvider().GetBytes(entropy);
|
||||
return entropy;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/Core/packages.config
Normal file
5
src/Core/packages.config
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle" version="1.8.1" targetFramework="net452" />
|
||||
<package id="Newtonsoft.Json" version="10.0.2" targetFramework="net452" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user