1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-10 05:13:17 +00:00

log in and store protected settings

This commit is contained in:
Kyle Spearrin
2017-05-12 10:15:34 -04:00
parent 0e8a5d229a
commit fa743815f8
17 changed files with 1038 additions and 103 deletions

View File

@@ -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("*");
}
}

View File

@@ -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; }
}
}

View File

@@ -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>

View 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; }
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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;
}
}
}

View 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; }
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}

View 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);
}
}
}

View File

@@ -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
View 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>