Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement user password change #730

Merged
merged 4 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions Crypter.API/Controllers/UserAuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using Crypter.API.Methods;
using Crypter.Common.Contracts;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using Crypter.Core.Features.UserAuthentication.Commands;
using Crypter.Core.Features.UserAuthentication.Queries;
using EasyMonads;
Expand Down Expand Up @@ -110,8 +111,7 @@ IActionResult MakeErrorResponse(LoginError error)
return error switch
{
LoginError.UnknownError
or LoginError.PasswordHashFailure => MakeErrorResponseBase(HttpStatusCode.InternalServerError,
error),
or LoginError.PasswordHashFailure => MakeErrorResponseBase(HttpStatusCode.InternalServerError, error),
LoginError.InvalidUsername
or LoginError.InvalidPassword
or LoginError.InvalidTokenTypeRequested
Expand Down Expand Up @@ -208,6 +208,40 @@ IActionResult MakeErrorResponse(PasswordChallengeError error)
MakeErrorResponse(PasswordChallengeError.UnknownError));
}

/// <summary>
/// Handle a request to change the password for an authorized user
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("password")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))]
public async Task<IActionResult> PasswordChangeAsync([FromBody] PasswordChangeRequest request)
{
IActionResult MakeErrorResponse(PasswordChangeError error)
{
#pragma warning disable CS8524
return error switch
{
PasswordChangeError.UnknownError
or PasswordChangeError.PasswordHashFailure => MakeErrorResponseBase(HttpStatusCode.InternalServerError, error),
PasswordChangeError.InvalidPassword
or PasswordChangeError.InvalidOldPasswordVersion
or PasswordChangeError.InvalidNewPasswordVersion => MakeErrorResponseBase(HttpStatusCode.BadRequest, error)
};
#pragma warning restore CS8524
}

ChangeUserPasswordCommand command = new ChangeUserPasswordCommand(UserId, request);
return await _sender.Send(command)
.MatchAsync(
MakeErrorResponse,
_ => Ok(),
MakeErrorResponse(PasswordChangeError.UnknownError));
}

/// <summary>
/// Clears the provided refresh token from the database, ensuring it cannot be used for subsequent requests.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using Crypter.Common.Client.Interfaces.HttpClients;
using Crypter.Common.Client.Interfaces.Requests;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using EasyMonads;

namespace Crypter.Common.Client.HttpClients.Requests;
Expand Down Expand Up @@ -77,14 +78,20 @@ public async Task<Either<RefreshError, RefreshResponse>> RefreshSessionAsync()
return response;
}

public Task<Either<PasswordChallengeError, Unit>> PasswordChallengeAsync(
PasswordChallengeRequest testPasswordRequest)
public Task<Either<PasswordChallengeError, Unit>> PasswordChallengeAsync(PasswordChallengeRequest testPasswordRequest)
{
const string url = "api/user/authentication/password/challenge";
return _crypterAuthenticatedHttpClient.PostEitherUnitResponseAsync(url, testPasswordRequest)
.ExtractErrorCode<PasswordChallengeError, Unit>();
}

public Task<Either<PasswordChangeError, Unit>> ChangePasswordAsync(PasswordChangeRequest passwordChangeRequest)
{
const string url = "api/user/authentication/password";
return _crypterAuthenticatedHttpClient.PostEitherUnitResponseAsync(url, passwordChangeRequest)
.ExtractErrorCode<PasswordChangeError, Unit>();
}

public Task<Either<LogoutError, Unit>> LogoutAsync()
{
const string url = "api/user/authentication/logout";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public Task<Either<InsertMasterKeyError, Unit>> InsertMasterKeyAsync(InsertMaste
.ExtractErrorCode<InsertMasterKeyError, Unit>();
}

public Task<Either<GetMasterKeyRecoveryProofError, GetMasterKeyRecoveryProofResponse>>
GetMasterKeyRecoveryProofAsync(GetMasterKeyRecoveryProofRequest request)
public Task<Either<GetMasterKeyRecoveryProofError, GetMasterKeyRecoveryProofResponse>> GetMasterKeyRecoveryProofAsync(GetMasterKeyRecoveryProofRequest request)
{
const string url = "api/user/key/master/recovery-proof/challenge";
return _crypterAuthenticatedHttpClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using System;
using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using EasyMonads;

namespace Crypter.Common.Client.Interfaces.Requests;
Expand All @@ -38,5 +39,6 @@ public interface IUserAuthenticationRequests
Task<Either<LoginError, LoginResponse>> LoginAsync(LoginRequest loginRequest);
Task<Either<RefreshError, RefreshResponse>> RefreshSessionAsync();
Task<Either<PasswordChallengeError, Unit>> PasswordChallengeAsync(PasswordChallengeRequest testPasswordRequest);
Task<Either<PasswordChangeError, Unit>> ChangePasswordAsync(PasswordChangeRequest passwordChangeRequest);
Task<Either<LogoutError, Unit>> LogoutAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ public interface IUserKeyRequests
Task<Either<GetMasterKeyError, GetMasterKeyResponse>> GetMasterKeyAsync();
Task<Either<InsertMasterKeyError, Unit>> InsertMasterKeyAsync(InsertMasterKeyRequest request);

Task<Either<GetMasterKeyRecoveryProofError, GetMasterKeyRecoveryProofResponse>> GetMasterKeyRecoveryProofAsync(
GetMasterKeyRecoveryProofRequest request);
Task<Either<GetMasterKeyRecoveryProofError, GetMasterKeyRecoveryProofResponse>> GetMasterKeyRecoveryProofAsync(GetMasterKeyRecoveryProofRequest request);

Task<Either<GetPrivateKeyError, GetPrivateKeyResponse>> GetPrivateKeyAsync();
Task<Either<InsertKeyPairError, Unit>> InsertKeyPairAsync(InsertKeyPairRequest request);
Expand Down
9 changes: 3 additions & 6 deletions Crypter.Common.Client/Interfaces/Services/IUserKeysService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ public interface IUserKeysService

Task DownloadExistingKeysAsync(Username username, Password password, bool trustDevice);
Task DownloadExistingKeysAsync(byte[] credentialKey, bool trustDevice);

Task<Maybe<RecoveryKey>> UploadNewKeysAsync(Username username, Password password,
VersionedPassword versionedPassword, bool trustDevice);

Task<Maybe<RecoveryKey>> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey,
bool trustDevice);

Task<Maybe<RecoveryKey>> UploadNewKeysAsync(Username username, Password password, VersionedPassword versionedPassword, bool trustDevice);
Task<Maybe<RecoveryKey>> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey, bool trustDevice);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
* Crypter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Crypter source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the aforementioned license
* by purchasing a commercial license. Buying such a license is mandatory
* as soon as you develop commercial activities involving the Crypter source
* code without disclosing the source code of your own applications.
*
* Contact the current copyright holder to discuss commercial license options.
*/

using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using Crypter.Common.Primitives;
using EasyMonads;

namespace Crypter.Common.Client.Interfaces.Services.UserSettings;

public interface IUserPasswordChangeService
{
Task<Either<PasswordChangeError, Unit>> ChangePasswordAsync(Password oldPassword, Password newPassword);
}
15 changes: 5 additions & 10 deletions Crypter.Common.Client/Services/UserKeysService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,33 +93,29 @@ public Task DownloadExistingKeysAsync(byte[] credentialKey, bool trustDevice)
private Task<Maybe<byte[]>> DownloadAndDecryptMasterKey(byte[] credentialKey)
{
return _crypterApiClient.UserKey.GetMasterKeyAsync()
.MapAsync<GetMasterKeyError, GetMasterKeyResponse, byte[]>(x =>
_cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey))
.MapAsync<GetMasterKeyError, GetMasterKeyResponse, byte[]>(x => _cryptoProvider.Encryption.Decrypt(credentialKey, x.Nonce, x.EncryptedKey))
.ToMaybeTask();
}

private Task<Maybe<byte[]>> DownloadAndDecryptPrivateKey(byte[] masterKey)
{
return _crypterApiClient.UserKey.GetPrivateKeyAsync()
.MapAsync<GetPrivateKeyError, GetPrivateKeyResponse, byte[]>(x =>
_cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey))
.MapAsync<GetPrivateKeyError, GetPrivateKeyResponse, byte[]>(x => _cryptoProvider.Encryption.Decrypt(masterKey, x.Nonce, x.EncryptedKey))
.ToMaybeTask();
}

#endregion

#region Upload New Keys

public Task<Maybe<RecoveryKey>> UploadNewKeysAsync(Username username, Password password,
VersionedPassword versionedPassword, bool trustDevice)
public Task<Maybe<RecoveryKey>> UploadNewKeysAsync(Username username, Password password, VersionedPassword versionedPassword, bool trustDevice)
{
return _userPasswordService
.DeriveUserCredentialKeyAsync(username, password, _userPasswordService.CurrentPasswordVersion)
.BindAsync(credentialKey => UploadNewKeysAsync(versionedPassword, credentialKey, trustDevice));
}

public Task<Maybe<RecoveryKey>> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey,
bool trustDevice)
public Task<Maybe<RecoveryKey>> UploadNewKeysAsync(VersionedPassword versionedPassword, byte[] credentialKey, bool trustDevice)
{
return UploadNewMasterKeyAsync(versionedPassword, credentialKey)
.BindAsync(recoveryKey => UploadNewUserKeyPairAsync(recoveryKey.MasterKey)
Expand All @@ -134,8 +130,7 @@ private Task<Maybe<RecoveryKey>> UploadNewMasterKeyAsync(VersionedPassword versi
byte[] encryptedMasterKey = _cryptoProvider.Encryption.Encrypt(credentialKey, nonce, newMasterKey);
byte[] recoveryProof = _cryptoProvider.Random.GenerateRandomBytes(32);

InsertMasterKeyRequest request =
new InsertMasterKeyRequest(versionedPassword.Password, encryptedMasterKey, nonce, recoveryProof);
InsertMasterKeyRequest request = new InsertMasterKeyRequest(versionedPassword.Password, encryptedMasterKey, nonce, recoveryProof);
return _crypterApiClient.UserKey.InsertMasterKeyAsync(request)
.ToMaybeTask()
.BindAsync(_ => new RecoveryKey(newMasterKey, request.RecoveryProof));
Expand Down
6 changes: 2 additions & 4 deletions Crypter.Common.Client/Services/UserPasswordService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ public Task<Maybe<byte[]>> DeriveUserCredentialKeyAsync(Username username, Passw
};
}

public async Task<Maybe<VersionedPassword>> DeriveUserAuthenticationPasswordAsync(Username username,
Password password, int passwordVersion)
public async Task<Maybe<VersionedPassword>> DeriveUserAuthenticationPasswordAsync(Username username, Password password, int passwordVersion)
{
#pragma warning disable CS0618
return passwordVersion switch
Expand All @@ -101,8 +100,7 @@ private VersionedPassword DeriveSha512AuthenticationPassword(Username username,
return new VersionedPassword(hashedPassword, 0);
}

private async Task<Maybe<VersionedPassword>> DeriveArgonAuthenticationPasswordAsync(Username username,
Password password)
private async Task<Maybe<VersionedPassword>> DeriveArgonAuthenticationPasswordAsync(Username username, Password password)
{
OnPasswordHashBeginEvent(PasswordHashType.AuthenticationKey);
await Task.Delay(1);
Expand Down
12 changes: 4 additions & 8 deletions Crypter.Common.Client/Services/UserRecoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,12 @@ public async Task<Either<SubmitAccountRecoveryError, Maybe<RecoveryKey>>> Submit
},
x =>
{
byte[] nonce =
_cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize);
byte[] encryptedMasterKey =
_cryptoProvider.Encryption.Encrypt(derivatives.CredentialKey, nonce, x.MasterKey);
byte[] nonce = _cryptoProvider.Random.GenerateRandomBytes((int)_cryptoProvider.Encryption.NonceSize);
byte[] encryptedMasterKey = _cryptoProvider.Encryption.Encrypt(derivatives.CredentialKey, nonce, x.MasterKey);
byte[] newRecoveryProof = _cryptoProvider.Random.GenerateRandomBytes(32);

RecoveryKey newRecoveryKey = new RecoveryKey(x.MasterKey, newRecoveryProof);
ReplacementMasterKeyInformation replacementMasterKeyInformation = new ReplacementMasterKeyInformation(x.Proof,
newRecoveryProof, encryptedMasterKey, nonce);
ReplacementMasterKeyInformation replacementMasterKeyInformation = new ReplacementMasterKeyInformation(x.Proof, newRecoveryProof, encryptedMasterKey, nonce);

return new RecoveryParameters
{
Expand Down Expand Up @@ -121,8 +118,7 @@ public Task<Maybe<RecoveryKey>> DeriveRecoveryKeyAsync(byte[] masterKey, Usernam

public Task<Maybe<RecoveryKey>> DeriveRecoveryKeyAsync(byte[] masterKey, VersionedPassword versionedPassword)
{
GetMasterKeyRecoveryProofRequest request =
new GetMasterKeyRecoveryProofRequest(versionedPassword.Password);
GetMasterKeyRecoveryProofRequest request = new GetMasterKeyRecoveryProofRequest(versionedPassword.Password);
return _crypterApiClient.UserKey.GetMasterKeyRecoveryProofAsync(request)
.ToMaybeTask()
.MapAsync(x => new RecoveryKey(masterKey, x.Proof));
Expand Down
Loading
Loading