From 14f952fdb01d88020e92eca4b7ea857fc0f101af Mon Sep 17 00:00:00 2001 From: Ben Fortune Date: Wed, 22 Apr 2020 14:36:37 +0100 Subject: [PATCH 1/5] Add optional MFA token to login requests --- GlobalAssemblyInfo.cs | 6 +-- MegaApiClient.Tests/Login.cs | 6 +-- .../MegaApiClientAsyncWrapper.cs | 24 ++++++++- MegaApiClient/Interface/IMegaApiClient.cs | 8 +-- .../Interface/IMegaApiClientAsync.cs | 4 +- MegaApiClient/MegaApiClient.cs | 50 +++++++++++++++++-- MegaApiClient/MegaApiClientAsync.cs | 13 +++-- MegaApiClient/Serialization/Login.cs | 11 ++++ 8 files changed, 103 insertions(+), 19 deletions(-) diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs index c887db2..e466a71 100644 --- a/GlobalAssemblyInfo.cs +++ b/GlobalAssemblyInfo.cs @@ -5,7 +5,7 @@ //------------------------------------------------------------------------------ using System.Reflection; -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0.0-develop")] +[assembly: AssemblyVersion("1.8.1.0")] +[assembly: AssemblyFileVersion("1.8.1.0")] +[assembly: AssemblyInformationalVersion("1.8.1+2.Branch.master.Sha.1614e6631fabe86015f3718ff8a169ac5cee1fc1")] diff --git a/MegaApiClient.Tests/Login.cs b/MegaApiClient.Tests/Login.cs index d566694..abae47d 100644 --- a/MegaApiClient.Tests/Login.cs +++ b/MegaApiClient.Tests/Login.cs @@ -195,7 +195,7 @@ public void Login_NullAuthInfos_Throws() [Theory, MemberData(nameof(AllValidCredentials))] public void Login_DeserializedAuthInfos_Succeeds(string email, string password) { - var authInfos = this.context.Client.GenerateAuthInfos(email, password); + var authInfos = this.context.Client.GenerateAuthInfos(email, password, null); var serializedAuthInfos = JsonConvert.SerializeObject(authInfos, Formatting.None).Replace('\"', '\''); var deserializedAuthInfos = JsonConvert.DeserializeObject(serializedAuthInfos); @@ -206,13 +206,13 @@ public void Login_DeserializedAuthInfos_Succeeds(string email, string password) [Theory, MemberData(nameof(InvalidCredentials))] public void GenerateAuthInfos_InvalidCredentials_Throws(string email, string password, string expectedMessage) { - Assert.Throws(expectedMessage, () => this.context.Client.GenerateAuthInfos(email, password)); + Assert.Throws(expectedMessage, () => this.context.Client.GenerateAuthInfos(email, password, null)); } [Theory, InlineData("username@example.com", "password", "{'Email':'username@example.com','Hash':'ObELy57HULI','PasswordAesKey':'ZAM5cl5uvROiXwBSEp98sQ=='}")] public void GenerateAuthInfos_ValidCredentials_Succeeds(string email, string password, string expectedResult) { - var authInfos = this.context.Client.GenerateAuthInfos(email, password); + var authInfos = this.context.Client.GenerateAuthInfos(email, password, null); var result = JsonConvert.SerializeObject(authInfos, Formatting.None).Replace('\"', '\''); Assert.Equal(expectedResult, result); diff --git a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs index dbf4acc..1701c0c 100644 --- a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs +++ b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs @@ -30,7 +30,12 @@ public bool IsLoggedIn public MegaApiClient.LogonSessionToken Login(string email, string password) { - return this.UnwrapException(() => this.client.LoginAsync(email, password).Result); + return this.UnwrapException(() => this.client.LoginAsync(email, password, null).Result); + } + + public MegaApiClient.LogonSessionToken Login(string email, string password, string mfaKey) + { + return this.UnwrapException(() => this.client.LoginAsync(email, password, mfaKey).Result); } public MegaApiClient.LogonSessionToken Login(MegaApiClient.AuthInfos authInfos) @@ -159,9 +164,19 @@ public MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password) return this.UnwrapException(() => this.client.GenerateAuthInfosAsync(email, password).Result); } + public MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password, string mfaKey) + { + return this.UnwrapException(() => this.client.GenerateAuthInfosAsync(email, password, mfaKey).Result); + } + public Task LoginAsync(string email, string password) { - return this.client.LoginAsync(email, password); + return this.client.LoginAsync(email, password, null); + } + + public Task LoginAsync(string email, string password, string mfaKey) + { + return this.client.LoginAsync(email, password, mfaKey); } public Task LoginAsync(MegaApiClient.AuthInfos authInfos) @@ -284,6 +299,11 @@ public Task> GetNodesFromLinkAsync(Uri uri) return this.client.GenerateAuthInfosAsync(email, password); } + public Task GenerateAuthInfosAsync(string email, string password, string mfaKey) + { + return this.client.GenerateAuthInfosAsync(email, password, mfaKey); + } + private T UnwrapException(Func action) { try diff --git a/MegaApiClient/Interface/IMegaApiClient.cs b/MegaApiClient/Interface/IMegaApiClient.cs index bc0120d..435d0e4 100644 --- a/MegaApiClient/Interface/IMegaApiClient.cs +++ b/MegaApiClient/Interface/IMegaApiClient.cs @@ -1,4 +1,4 @@ -namespace CG.Web.MegaApiClient +namespace CG.Web.MegaApiClient { using System; using System.Collections.Generic; @@ -13,6 +13,8 @@ public partial interface IMegaApiClient MegaApiClient.LogonSessionToken Login(string email, string password); + MegaApiClient.LogonSessionToken Login(string email, string password, string mfaKey); + MegaApiClient.LogonSessionToken Login(MegaApiClient.AuthInfos authInfos); void Login(MegaApiClient.LogonSessionToken logonSessionToken); @@ -59,6 +61,6 @@ public partial interface IMegaApiClient INode Rename(INode node, string newName); - MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password); + MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password, string mfaKey); } -} \ No newline at end of file +} diff --git a/MegaApiClient/Interface/IMegaApiClientAsync.cs b/MegaApiClient/Interface/IMegaApiClientAsync.cs index f097406..f71682d 100644 --- a/MegaApiClient/Interface/IMegaApiClientAsync.cs +++ b/MegaApiClient/Interface/IMegaApiClientAsync.cs @@ -9,7 +9,7 @@ namespace CG.Web.MegaApiClient public partial interface IMegaApiClient { - Task LoginAsync(string email, string password); + Task LoginAsync(string email, string password, string mfaKey); Task LoginAsync(MegaApiClient.AuthInfos authInfos); @@ -58,6 +58,8 @@ public partial interface IMegaApiClient Task> GetNodesFromLinkAsync(Uri uri); Task GenerateAuthInfosAsync(string email, string password); + + Task GenerateAuthInfosAsync(string email, string password, string mfaKey); } } #endif diff --git a/MegaApiClient/MegaApiClient.cs b/MegaApiClient/MegaApiClient.cs index 1b728fe..f080686 100644 --- a/MegaApiClient/MegaApiClient.cs +++ b/MegaApiClient/MegaApiClient.cs @@ -88,9 +88,10 @@ public MegaApiClient(Options options, IWebClient webClient) /// /// email /// password + /// /// object containing encrypted data /// email or password is null - public AuthInfos GenerateAuthInfos(string email, string password) + public AuthInfos GenerateAuthInfos(string email, string password, string mfaKey = null) { if (string.IsNullOrEmpty(email)) { @@ -121,6 +122,14 @@ public AuthInfos GenerateAuthInfos(string email, string password) } // Derived key contains master key (0-16) and password hash (16-32) + if(!string.IsNullOrEmpty(mfaKey)) + { + return new AuthInfos( + email, + derivedKeyBytes.Skip(16).ToArray().ToBase64(), + derivedKeyBytes.Take(16).ToArray(), + mfaKey); + } return new AuthInfos( email, derivedKeyBytes.Skip(16).ToArray().ToBase64(), @@ -136,7 +145,10 @@ public AuthInfos GenerateAuthInfos(string email, string password) // Hash email and password to decrypt master key on Mega servers string hash = GenerateHash(email.ToLowerInvariant(), passwordAesKey); - + if (!string.IsNullOrEmpty(mfaKey)) + { + return new AuthInfos(email, hash, passwordAesKey, mfaKey); + } return new AuthInfos(email, hash, passwordAesKey); } else @@ -165,6 +177,20 @@ public LogonSessionToken Login(string email, string password) return this.Login(GenerateAuthInfos(email, password)); } + /// + /// Login to Mega.co.nz service using email/password credentials + /// + /// email + /// password + /// + /// Service is not available or credentials are invalid + /// email or password is null + /// Already logged in + public LogonSessionToken Login(string email, string password, string mfaKey) + { + return this.Login(GenerateAuthInfos(email, password, mfaKey)); + } + /// /// Login to Mega.co.nz service using hashed credentials /// @@ -183,7 +209,14 @@ public LogonSessionToken Login(AuthInfos authInfos) this.authenticatedLogin = true; // Request Mega Api - LoginRequest request = new LoginRequest(authInfos.Email, authInfos.Hash); + LoginRequest request; + if (!string.IsNullOrEmpty(authInfos.MFAKey)) + { + request = new LoginRequest(authInfos.Email, authInfos.Hash, authInfos.MFAKey); + } else + { + request = new LoginRequest(authInfos.Email, authInfos.Hash); + } LoginResponse response = this.Request(request); // Decrypt master key using our password key @@ -1214,6 +1247,14 @@ public AuthInfos(string email, string hash, byte[] passwordAesKey) this.PasswordAesKey = passwordAesKey; } + public AuthInfos(string email, string hash, byte[] passwordAesKey, string mfaKey) + { + this.Email = email; + this.Hash = hash; + this.PasswordAesKey = passwordAesKey; + this.MFAKey = mfaKey; + } + [JsonProperty] public string Email { get; private set; } @@ -1222,6 +1263,9 @@ public AuthInfos(string email, string hash, byte[] passwordAesKey) [JsonProperty] public byte[] PasswordAesKey { get; private set; } + + [JsonProperty] + public string MFAKey { get; private set; } } public class LogonSessionToken : IEquatable diff --git a/MegaApiClient/MegaApiClientAsync.cs b/MegaApiClient/MegaApiClientAsync.cs index 76f0d5c..f39a661 100644 --- a/MegaApiClient/MegaApiClientAsync.cs +++ b/MegaApiClient/MegaApiClientAsync.cs @@ -9,11 +9,11 @@ namespace CG.Web.MegaApiClient public partial class MegaApiClient : IMegaApiClient { - #region Public async methods +#region Public async methods - public Task LoginAsync(string email, string password) + public Task LoginAsync(string email, string password, string mfaKey = null) { - return Task.Run(() => this.Login(email, password)); + return Task.Run(() => this.Login(email, password, mfaKey)); } public Task LoginAsync(AuthInfos authInfos) @@ -177,7 +177,12 @@ public Task> GetNodesFromLinkAsync(Uri uri) return Task.Run(() => this.GenerateAuthInfos(email, password)); } - #endregion + public Task GenerateAuthInfosAsync(string email, string password, string mfaKey) + { + return Task.Run(() => this.GenerateAuthInfos(email, password, mfaKey)); + } + +#endregion } } #endif diff --git a/MegaApiClient/Serialization/Login.cs b/MegaApiClient/Serialization/Login.cs index 8f0f9a7..dc1f967 100644 --- a/MegaApiClient/Serialization/Login.cs +++ b/MegaApiClient/Serialization/Login.cs @@ -11,11 +11,22 @@ public LoginRequest(string userHandle, string passwordHash) this.PasswordHash = passwordHash; } + public LoginRequest(string userHandle, string passwordHash, string mfaKey) + : base("us") + { + this.UserHandle = userHandle; + this.PasswordHash = passwordHash; + this.MFAKey = mfaKey; + } + [JsonProperty("user")] public string UserHandle { get; private set; } [JsonProperty("uh")] public string PasswordHash { get; private set; } + + [JsonProperty("mfa")] + public string MFAKey { get; private set; } } internal class LoginResponse From fb1aec241a71c9edc0a4ac394206504d4e2fe8df Mon Sep 17 00:00:00 2001 From: Ben Fortune Date: Wed, 22 Apr 2020 14:47:39 +0100 Subject: [PATCH 2/5] Revert GlobalAssemblyInfo.cs for author --- GlobalAssemblyInfo.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs index e466a71..a721ee7 100644 --- a/GlobalAssemblyInfo.cs +++ b/GlobalAssemblyInfo.cs @@ -5,7 +5,6 @@ //------------------------------------------------------------------------------ using System.Reflection; -[assembly: AssemblyVersion("1.8.1.0")] -[assembly: AssemblyFileVersion("1.8.1.0")] -[assembly: AssemblyInformationalVersion("1.8.1+2.Branch.master.Sha.1614e6631fabe86015f3718ff8a169ac5cee1fc1")] - +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0-develop")] From 9b78142bcc989bfba0dfdf1e7cc66cb3312431c3 Mon Sep 17 00:00:00 2001 From: Ben Fortune Date: Wed, 22 Apr 2020 15:10:05 +0100 Subject: [PATCH 3/5] Make it optional instead of an overload, revert tests --- MegaApiClient.Tests/Login.cs | 6 +++--- MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs | 2 +- MegaApiClient/Interface/IMegaApiClient.cs | 2 +- MegaApiClient/Interface/IMegaApiClientAsync.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MegaApiClient.Tests/Login.cs b/MegaApiClient.Tests/Login.cs index abae47d..d566694 100644 --- a/MegaApiClient.Tests/Login.cs +++ b/MegaApiClient.Tests/Login.cs @@ -195,7 +195,7 @@ public void Login_NullAuthInfos_Throws() [Theory, MemberData(nameof(AllValidCredentials))] public void Login_DeserializedAuthInfos_Succeeds(string email, string password) { - var authInfos = this.context.Client.GenerateAuthInfos(email, password, null); + var authInfos = this.context.Client.GenerateAuthInfos(email, password); var serializedAuthInfos = JsonConvert.SerializeObject(authInfos, Formatting.None).Replace('\"', '\''); var deserializedAuthInfos = JsonConvert.DeserializeObject(serializedAuthInfos); @@ -206,13 +206,13 @@ public void Login_DeserializedAuthInfos_Succeeds(string email, string password) [Theory, MemberData(nameof(InvalidCredentials))] public void GenerateAuthInfos_InvalidCredentials_Throws(string email, string password, string expectedMessage) { - Assert.Throws(expectedMessage, () => this.context.Client.GenerateAuthInfos(email, password, null)); + Assert.Throws(expectedMessage, () => this.context.Client.GenerateAuthInfos(email, password)); } [Theory, InlineData("username@example.com", "password", "{'Email':'username@example.com','Hash':'ObELy57HULI','PasswordAesKey':'ZAM5cl5uvROiXwBSEp98sQ=='}")] public void GenerateAuthInfos_ValidCredentials_Succeeds(string email, string password, string expectedResult) { - var authInfos = this.context.Client.GenerateAuthInfos(email, password, null); + var authInfos = this.context.Client.GenerateAuthInfos(email, password); var result = JsonConvert.SerializeObject(authInfos, Formatting.None).Replace('\"', '\''); Assert.Equal(expectedResult, result); diff --git a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs index 1701c0c..ff1a681 100644 --- a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs +++ b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs @@ -30,7 +30,7 @@ public bool IsLoggedIn public MegaApiClient.LogonSessionToken Login(string email, string password) { - return this.UnwrapException(() => this.client.LoginAsync(email, password, null).Result); + return this.UnwrapException(() => this.client.LoginAsync(email, password).Result); } public MegaApiClient.LogonSessionToken Login(string email, string password, string mfaKey) diff --git a/MegaApiClient/Interface/IMegaApiClient.cs b/MegaApiClient/Interface/IMegaApiClient.cs index 435d0e4..e63fee8 100644 --- a/MegaApiClient/Interface/IMegaApiClient.cs +++ b/MegaApiClient/Interface/IMegaApiClient.cs @@ -61,6 +61,6 @@ public partial interface IMegaApiClient INode Rename(INode node, string newName); - MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password, string mfaKey); + MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password, string mfaKey = null); } } diff --git a/MegaApiClient/Interface/IMegaApiClientAsync.cs b/MegaApiClient/Interface/IMegaApiClientAsync.cs index f71682d..60ee04d 100644 --- a/MegaApiClient/Interface/IMegaApiClientAsync.cs +++ b/MegaApiClient/Interface/IMegaApiClientAsync.cs @@ -9,7 +9,7 @@ namespace CG.Web.MegaApiClient public partial interface IMegaApiClient { - Task LoginAsync(string email, string password, string mfaKey); + Task LoginAsync(string email, string password, string mfaKey = null); Task LoginAsync(MegaApiClient.AuthInfos authInfos); From 12978bc06c047c20e7da5ac899323117c68a406b Mon Sep 17 00:00:00 2001 From: Ben Fortune Date: Wed, 22 Apr 2020 15:12:42 +0100 Subject: [PATCH 4/5] Revert old tests --- MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs index ff1a681..1599506 100644 --- a/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs +++ b/MegaApiClient.Tests/MegaApiClientAsyncWrapper.cs @@ -171,7 +171,7 @@ public MegaApiClient.AuthInfos GenerateAuthInfos(string email, string password, public Task LoginAsync(string email, string password) { - return this.client.LoginAsync(email, password, null); + return this.client.LoginAsync(email, password); } public Task LoginAsync(string email, string password, string mfaKey) From 0dea838c558e03265e868086bb2572d8cdf34f7e Mon Sep 17 00:00:00 2001 From: Ben Fortune Date: Thu, 23 Apr 2020 14:00:03 +0100 Subject: [PATCH 5/5] Some style fixes --- MegaApiClient/MegaApiClient.cs | 3 ++- MegaApiClient/Serialization/Login.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MegaApiClient/MegaApiClient.cs b/MegaApiClient/MegaApiClient.cs index f080686..1166d5a 100644 --- a/MegaApiClient/MegaApiClient.cs +++ b/MegaApiClient/MegaApiClient.cs @@ -213,7 +213,8 @@ public LogonSessionToken Login(AuthInfos authInfos) if (!string.IsNullOrEmpty(authInfos.MFAKey)) { request = new LoginRequest(authInfos.Email, authInfos.Hash, authInfos.MFAKey); - } else + } + else { request = new LoginRequest(authInfos.Email, authInfos.Hash); } diff --git a/MegaApiClient/Serialization/Login.cs b/MegaApiClient/Serialization/Login.cs index dc1f967..c8a3921 100644 --- a/MegaApiClient/Serialization/Login.cs +++ b/MegaApiClient/Serialization/Login.cs @@ -12,7 +12,7 @@ public LoginRequest(string userHandle, string passwordHash) } public LoginRequest(string userHandle, string passwordHash, string mfaKey) - : base("us") + : base("us") { this.UserHandle = userHandle; this.PasswordHash = passwordHash;