mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-10 13:23:18 +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;
|
private static string[] _args = null;
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
MainAsync(args).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task MainAsync(string[] args)
|
||||||
{
|
{
|
||||||
_args = args;
|
_args = args;
|
||||||
_usingArgs = args.Length > 0;
|
_usingArgs = args.Length > 0;
|
||||||
@@ -22,6 +27,8 @@ namespace Bit.Console
|
|||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
|
Con.ResetColor();
|
||||||
|
|
||||||
if(_usingArgs)
|
if(_usingArgs)
|
||||||
{
|
{
|
||||||
selection = args[0];
|
selection = args[0];
|
||||||
@@ -45,10 +52,12 @@ namespace Bit.Console
|
|||||||
{
|
{
|
||||||
case "1":
|
case "1":
|
||||||
case "login":
|
case "login":
|
||||||
LogIn();
|
case "signin":
|
||||||
|
await LogInAsync();
|
||||||
break;
|
break;
|
||||||
case "2":
|
case "2":
|
||||||
case "dir":
|
case "dir":
|
||||||
|
case "directory":
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "3":
|
case "3":
|
||||||
@@ -57,6 +66,7 @@ namespace Bit.Console
|
|||||||
break;
|
break;
|
||||||
case "4":
|
case "4":
|
||||||
case "svc":
|
case "svc":
|
||||||
|
case "service":
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "5":
|
case "5":
|
||||||
@@ -84,37 +94,60 @@ namespace Bit.Console
|
|||||||
_args = null;
|
_args = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LogIn()
|
private static async Task LogInAsync()
|
||||||
{
|
{
|
||||||
string email = null;
|
string email = null;
|
||||||
SecureString masterPassword = null;
|
string masterPassword = null;
|
||||||
|
|
||||||
if(_usingArgs)
|
if(_usingArgs)
|
||||||
{
|
{
|
||||||
email = _args[1];
|
Con.ForegroundColor = ConsoleColor.Red;
|
||||||
masterPassword = new SecureString();
|
Con.WriteLine("You cannot log in via arguments. Use the console instead.");
|
||||||
foreach(var c in _args[2])
|
Con.ResetColor();
|
||||||
{
|
return;
|
||||||
masterPassword.AppendChar(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Con.Write("Email: ");
|
Con.Write("Email: ");
|
||||||
email = Con.ReadLine();
|
email = Con.ReadLine().Trim();
|
||||||
Con.Write("Master password: ");
|
Con.Write("Master password: ");
|
||||||
masterPassword = ReadSecureLine();
|
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)
|
while(true)
|
||||||
{
|
{
|
||||||
ConsoleKeyInfo i = Con.ReadKey(true);
|
var i = Con.ReadKey(true);
|
||||||
if(i.Key == ConsoleKey.Enter)
|
if(i.Key == ConsoleKey.Enter)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -123,13 +156,13 @@ namespace Bit.Console
|
|||||||
{
|
{
|
||||||
if(input.Length > 0)
|
if(input.Length > 0)
|
||||||
{
|
{
|
||||||
input.RemoveAt(input.Length - 1);
|
input = input.Remove(input.Length - 1);
|
||||||
Con.Write("\b \b");
|
Con.Write("\b \b");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
input.AppendChar(i.KeyChar);
|
input = string.Concat(input, i.KeyChar);
|
||||||
Con.Write("*");
|
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>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<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" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.DirectoryServices" />
|
<Reference Include="System.DirectoryServices" />
|
||||||
@@ -43,9 +49,25 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="TokenService.cs" />
|
<Compile Include="Models\ApiError.cs" />
|
||||||
<Compile Include="AuthService.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" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Utilities\" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</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