diff --git a/src/Console/Program.cs b/src/Console/Program.cs index cb402835..68173ecb 100644 --- a/src/Console/Program.cs +++ b/src/Console/Program.cs @@ -1,19 +1,16 @@ -using Bit.Core.Models; +using Bit.Core.Enums; +using Bit.Core.Models; using Bit.Core.Services; using Bit.Core.Utilities; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Security; -using System.Text; using System.Threading.Tasks; using Con = System.Console; namespace Bit.Console { - class Program + public class Program { private static bool _usingArgs = false; private static bool _exit = false; @@ -155,6 +152,7 @@ namespace Bit.Console string masterPassword = null; string token = null; string orgId = null; + var provider = TwoFactorProviderType.Authenticator; if(_usingArgs) { @@ -164,7 +162,8 @@ namespace Bit.Console email = parameters["e"]; masterPassword = parameters["p"]; } - if(parameters.Count >= 3 && parameters.ContainsKey("t")) + if(parameters.Count >= 4 && parameters.ContainsKey("t") && parameters.ContainsKey("tp") && + Enum.TryParse(parameters["tp"], out provider)) { token = parameters["t"]; } @@ -198,21 +197,81 @@ namespace Bit.Console } else { - result = await AuthService.Instance.LogInTwoFactorAsync(email, masterPassword, token); + result = await AuthService.Instance.LogInTwoFactorAsync(provider, token, + email, masterPassword); } if(string.IsNullOrWhiteSpace(token) && 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: "); - token = Con.ReadLine().Trim(); - result = await AuthService.Instance.LogInTwoFactorWithHashAsync(token, email, - result.MasterPasswordHash); + Con.WriteLine("Two-step login is enabled on this account."); + if(result.TwoFactorProviders.Count > 1) + { + for(var i = 0; i < result.TwoFactorProviders.Count; i++) + { + Con.WriteLine("{0}. {1}{2}", i + 1, result.TwoFactorProviders.ElementAt(i).Key, + result.TwoFactorProviders.ElementAt(i).Key == TwoFactorProviderType.Duo || + result.TwoFactorProviders.ElementAt(i).Key == TwoFactorProviderType.U2f ? + " - not supported" : string.Empty); + } + Con.WriteLine(); + Con.Write("Which provider would you like to use?: "); + var providerIndexInput = Con.ReadLine().Trim(); + int providerIndex; + if(int.TryParse(providerIndexInput, out providerIndex) && result.TwoFactorProviders.Count >= providerIndex) + { + provider = result.TwoFactorProviders.ElementAt(providerIndex - 1).Key; + } + else + { + provider = result.TwoFactorProviders.First().Key; + } + } + else + { + provider = result.TwoFactorProviders.First().Key; + Con.WriteLine(); + } + + var readingTokenInput = true; + if(provider == TwoFactorProviderType.YubiKey) + { + Con.Write("Tap the button on your YubiKey: "); + } + else if(provider == TwoFactorProviderType.Email) + { + if(result.TwoFactorProviders.Count > 1) + { + await ApiService.Instance.PostTwoFactorSendEmailLoginAsync(new TwoFactorEmailRequest + { + Email = email, + MasterPasswordHash = result.MasterPasswordHash + }); + } + + Con.Write("Enter the verification code that was emailed to you: "); + } + else if(provider == TwoFactorProviderType.Authenticator) + { + Con.Write("Enter the verification code from your authenticator app: "); + } + else + { + Con.WriteLine("The selected two-step login method is not supported on this platform/application. " + + "Use a different two step-login method."); + readingTokenInput = false; + } + + if(readingTokenInput) + { + token = Con.ReadLine().Trim(); + result = await AuthService.Instance.LogInTwoFactorWithHashAsync(provider, token, email, + result.MasterPasswordHash); + } } - if(result.Success && result.Organizations.Count > 1) + if(result.Success && result.Organizations != null && result.Organizations.Count > 1) { Organization org = null; if(!string.IsNullOrWhiteSpace(orgId)) @@ -253,8 +312,15 @@ namespace Bit.Console Con.WriteLine(); if(result.Success) { - WriteSuccessLine(string.Format("You have successfully logged in as {0}!", - TokenService.Instance.AccessTokenEmail)); + if(!result.TwoFactorRequired) + { + WriteSuccessLine(string.Format("You have successfully logged in as {0}!", + TokenService.Instance.AccessTokenEmail)); + } + else + { + WriteErrorLine("Unable to log in."); + } } else { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 6c7c410f..787b58f6 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -87,6 +87,7 @@ + @@ -107,6 +108,7 @@ + diff --git a/src/Core/Enums/TwoFactorProviderType.cs b/src/Core/Enums/TwoFactorProviderType.cs new file mode 100644 index 00000000..28be1b2c --- /dev/null +++ b/src/Core/Enums/TwoFactorProviderType.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Enums +{ + public enum TwoFactorProviderType : byte + { + Authenticator = 0, + Email = 1, + Duo = 2, + YubiKey = 3, + U2f = 4, + Remember = 5 + } +} diff --git a/src/Core/Models/LoginResult.cs b/src/Core/Models/LoginResult.cs index d84fb0b6..84bf6a99 100644 --- a/src/Core/Models/LoginResult.cs +++ b/src/Core/Models/LoginResult.cs @@ -1,4 +1,5 @@ -using System; +using Bit.Core.Enums; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -10,7 +11,8 @@ namespace Bit.Core.Models { public bool Success { get; set; } public string ErrorMessage { get; set; } - public bool TwoFactorRequired { get; set; } + public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0; + public Dictionary> TwoFactorProviders { get; set; } public string MasterPasswordHash { get; set; } public List Organizations { get; set; } } diff --git a/src/Core/Models/TokenRequest.cs b/src/Core/Models/TokenRequest.cs index e53584aa..8f7135e6 100644 --- a/src/Core/Models/TokenRequest.cs +++ b/src/Core/Models/TokenRequest.cs @@ -1,4 +1,5 @@ -using System; +using Bit.Core.Enums; +using System; using System.Collections.Generic; namespace Bit.Core.Models @@ -8,7 +9,8 @@ namespace Bit.Core.Models public string Email { get; set; } public string MasterPasswordHash { get; set; } public string Token { get; set; } - public int? Provider { get; set; } + public TwoFactorProviderType? Provider { get; set; } + public bool Remember { get; set; } public IDictionary ToIdentityTokenRequest() { @@ -24,7 +26,8 @@ namespace Bit.Core.Models if(Token != null && Provider.HasValue) { dict.Add("TwoFactorToken", Token); - dict.Add("TwoFactorProvider", Provider.Value.ToString()); + dict.Add("TwoFactorProvider", ((byte)(Provider.Value)).ToString()); + dict.Add("TwoFactorRemember", Remember ? "1" : "0"); } return dict; diff --git a/src/Core/Models/TokenResponse.cs b/src/Core/Models/TokenResponse.cs index 31f07ad2..0fe96b54 100644 --- a/src/Core/Models/TokenResponse.cs +++ b/src/Core/Models/TokenResponse.cs @@ -1,9 +1,6 @@ -using Newtonsoft.Json; -using System; +using Bit.Core.Enums; +using Newtonsoft.Json; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Bit.Core.Models { @@ -17,7 +14,9 @@ namespace Bit.Core.Models public string RefreshToken { get; set; } [JsonProperty("token_type")] public string TokenType { get; set; } - public List TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProviders2 { get; set; } public string PrivateKey { get; set; } + public string TwoFactorToken { get; set; } + public string Key { get; set; } } } diff --git a/src/Core/Models/TwoFactorEmailRequest.cs b/src/Core/Models/TwoFactorEmailRequest.cs new file mode 100644 index 00000000..d7a6a5ac --- /dev/null +++ b/src/Core/Models/TwoFactorEmailRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models +{ + public class TwoFactorEmailRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 36ff6fc1..a2d81fd7 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Models; +using Bit.Core.Enums; +using Bit.Core.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -51,11 +52,12 @@ namespace Bit.Core.Services if(!response.IsSuccessStatusCode) { var errorResponse = JObject.Parse(responseContent); - if(errorResponse["TwoFactorProviders"] != null) + if(errorResponse["TwoFactorProviders2"] != null) { return ApiResult.Success(new TokenResponse { - TwoFactorProviders = errorResponse["TwoFactorProviders"].ToObject>() + TwoFactorProviders2 = errorResponse["TwoFactorProviders2"] + .ToObject>>() }, response.StatusCode); } @@ -140,6 +142,33 @@ namespace Bit.Core.Services } } + public virtual async Task PostTwoFactorSendEmailLoginAsync(TwoFactorEmailRequest requestObj) + { + var stringContent = JsonConvert.SerializeObject(requestObj); + + var requestMessage = new HttpRequestMessage() + { + Method = HttpMethod.Post, + RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/two-factor/send-email-login")), + Content = new StringContent(stringContent, Encoding.UTF8, "application/json") + }; + + try + { + var response = await Client.SendAsync(requestMessage).ConfigureAwait(false); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response).ConfigureAwait(false); + } + + return ApiResult.Success(response.StatusCode); + } + catch + { + return HandledWebException(); + } + } + protected ApiResult HandledWebException() { return ApiResult.Failed(HttpStatusCode.BadGateway, diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 91c49be0..181af6e0 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -63,9 +63,9 @@ namespace Bit.Core.Services } result.Success = true; - if(response.Result.TwoFactorProviders != null && response.Result.TwoFactorProviders.Count > 0) + if(response.Result.TwoFactorProviders2 != null && response.Result.TwoFactorProviders2.Count > 0) { - result.TwoFactorRequired = true; + result.TwoFactorProviders = response.Result.TwoFactorProviders2; result.MasterPasswordHash = request.MasterPasswordHash; return result; } @@ -73,12 +73,13 @@ namespace Bit.Core.Services return await ProcessLogInSuccessAsync(response.Result); } - public async Task LogInTwoFactorAsync(string token, string email, string masterPassword) + public async Task LogInTwoFactorAsync(TwoFactorProviderType type, string token, string email, + string masterPassword) { var normalizedEmail = email.Trim().ToLower(); var key = Crypto.MakeKeyFromPassword(masterPassword, normalizedEmail); - var result = await LogInTwoFactorWithHashAsync(token, email, Crypto.HashPasswordBase64(key, masterPassword)); + var result = await LogInTwoFactorWithHashAsync(type, token, email, Crypto.HashPasswordBase64(key, masterPassword)); key = null; masterPassword = null; @@ -86,14 +87,21 @@ namespace Bit.Core.Services return result; } - public async Task LogInTwoFactorWithHashAsync(string token, string email, string masterPasswordHash) + public async Task LogInTwoFactorWithHashAsync(TwoFactorProviderType type, string token, string email, + string masterPasswordHash) { + if(type == TwoFactorProviderType.Email || type == TwoFactorProviderType.Authenticator) + { + token = token.Trim().Replace(" ", ""); + } + 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) + Token = token, + Provider = type, + Remember = false }; var response = await ApiService.Instance.PostTokenAsync(request);