/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Security; using ProtonVPN.Api.Contracts; using ProtonVPN.Api.Contracts.Auth; using ProtonVPN.Api.Contracts.Auth.Fido2; using ProtonVPN.Api.Contracts.Common; using ProtonVPN.Client.Logic.Auth.Contracts.Enums; using ProtonVPN.Client.Logic.Auth.Contracts.Models; using ProtonVPN.Client.Logic.Auth.Srp.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts.GuestHole; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.OperatingSystems.WebAuthn.Contracts; namespace ProtonVPN.Client.Logic.Auth; public class SrpAuthenticator : AuthenticatorBase, ISrpAuthenticator { private const string SRP_LOGIN_INTENT = "Proton"; private const int TYPE_2FA_TOTP = 1; private const int TYPE_2FA_FIDO2 = 2; private readonly IApiClient _apiClient; private readonly IUnauthSessionManager _unauthSessionManager; private readonly IWebAuthnAuthenticator _webAuthnAuthenticator; private readonly ISrpProofGenerator _srpProofGenerator; private AuthResponse? _authResponse; public bool IsTwoFactorAuthenticatorModeEnabled => _authResponse != null && (_authResponse.TwoFactor.Enabled & TYPE_2FA_TOTP) != 0; public bool IsTwoFactorSecurityKeyModeEnabled => _authResponse != null && (_authResponse.TwoFactor.Enabled & TYPE_2FA_FIDO2) != 0; public SrpAuthenticator( IApiClient apiClient, ISettings settings, IUnauthSessionManager unauthSessionManager, IGuestHoleManager guestHoleManager, IWebAuthnAuthenticator webAuthnAuthenticator, ISrpProofGenerator srpProofGenerator) : base(settings) { _apiClient = apiClient; _unauthSessionManager = unauthSessionManager; _webAuthnAuthenticator = webAuthnAuthenticator; _srpProofGenerator = srpProofGenerator; } public async Task LoginUserAsync(string username, SecureString password, CancellationToken cancellationToken) { await _unauthSessionManager.CreateIfDoesNotExistAsync(cancellationToken); ApiResponseResult authInfoResponse = await _apiClient.GetAuthInfoResponse( new AuthInfoRequest { Username = username, Intent = SRP_LOGIN_INTENT }, cancellationToken); if (!authInfoResponse.Success) { return AuthResult.Fail(authInfoResponse); } if (string.IsNullOrEmpty(authInfoResponse.Value.Salt)) { return AuthResult.Fail("Incorrect login credentials. Please try again"); } try { SrpProof? proof = _srpProofGenerator.GenerateProof(password, authInfoResponse.Value); if (proof == null) { return AuthResult.Fail(AuthError.Unknown); } AuthRequest authRequest = new() { ClientEphemeral = proof.ClientEphemeral, ClientProof = proof.ClientProof, SrpSession = authInfoResponse.Value.SrpSession, Username = username }; ApiResponseResult response = await _apiClient.GetAuthResponse(authRequest, cancellationToken); if (response.Failure) { return AuthResult.Fail(response); } if (proof.ExpectedServerProof != response.Value.ServerProof) { return AuthResult.Fail(AuthError.InvalidServerProof); } if (response.Value.TwoFactor.Enabled != 0) { _authResponse = response.Value; return AuthResult.Fail(AuthError.TwoFactorRequired); } SaveAuthSessionDetails(response.Value); return AuthResult.Ok(); } catch (DllNotFoundException) { return AuthResult.Fail(AuthError.MissingSrpDll); } } public async Task SendTwoFactorCodeAsync(string code, CancellationToken cancellationToken) { TwoFactorRequest request = new() { TwoFactorCode = code }; ApiResponseResult response = await _apiClient.GetTwoFactorAuthResponse( request, _authResponse?.AccessToken ?? string.Empty, _authResponse?.UniqueSessionId ?? string.Empty, cancellationToken); if (response.Failure) { return AuthResult.Fail(response.Value.Code == ResponseCodes.INCORRECT_LOGIN_CREDENTIALS ? AuthError.IncorrectTwoFactorCode : AuthError.TwoFactorAuthFailed); } SaveAuthSessionDetails(_authResponse); return AuthResult.Ok(); } public async Task AuthenticateWithSecurityKeyAsync(CancellationToken cancellationToken) { if (_authResponse == null || _authResponse.TwoFactor.Fido2 == null) { return AuthResult.Fail(AuthError.TwoFactorAuthFailed); } List allowedCredentials = _authResponse.TwoFactor.Fido2.AuthenticationOptions.PublicKey.AllowCredentials .Select(ac => new AllowedCredential(ac.Id.ToArray(), ac.Type)).ToList(); WebAuthnResponse authResult = await _webAuthnAuthenticator.AuthenticateAsync( rpId: _authResponse.TwoFactor.Fido2.AuthenticationOptions.PublicKey.RpId, challenge: _authResponse.TwoFactor.Fido2.AuthenticationOptions.PublicKey.Challenge.ToArray(), userVerificationRequirement: _authResponse.TwoFactor.Fido2.AuthenticationOptions.PublicKey.UserVerification, timeoutInMilliseconds: _authResponse.TwoFactor.Fido2.AuthenticationOptions.PublicKey.Timeout, allowedCredentials: allowedCredentials, cancellationToken: cancellationToken); if (authResult is null) { return AuthResult.Fail(AuthError.WebAuthnNotSupported); } TwoFactorRequest request = new() { TwoFactorCode = null, Fido2 = new Fido2Request() { AuthenticationOptions = _authResponse.TwoFactor.Fido2.AuthenticationOptions, ClientData = Convert.ToBase64String(authResult.ClientDataJson), AuthenticatorData = Convert.ToBase64String(authResult.AuthenticatorData), Signature = Convert.ToBase64String(authResult.Signature), CredentialId = authResult.CredentialId.ToList(), }, }; ApiResponseResult response = await _apiClient.GetTwoFactorAuthResponse( request, _authResponse?.AccessToken ?? string.Empty, _authResponse?.UniqueSessionId ?? string.Empty, cancellationToken); if (response.Failure) { return AuthResult.Fail(AuthError.TwoFactorAuthFailed); } SaveAuthSessionDetails(_authResponse); return AuthResult.Ok(); } }