/// <summary> /// Changes the user password.<para> </para> /// Remember that <paramref name="requestBody"/>'s field /// <see cref="EpistleRequestBody.Body"/> should be the <see cref="UserChangePasswordRequestDto"/> /// instance serialized into JSON and then compressed! /// </summary> /// <param name="requestBody">Request parameters DTO wrapped into an <see cref="EpistleRequestBody"/>.</param> /// <returns><c>bool</c> indicating whether the change was successful or not.</returns> public async Task <bool> ChangeUserPassword(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "users/change-pw", Method.Put); RestResponse response = await restClient.ExecuteAsync(request).ConfigureAwait(false); return(response?.StatusCode == HttpStatusCode.OK); }
/// <summary> /// Deletes a user irreversibly from the backend's db.<para> </para> /// <paramref name="requestBody.Body"/> should directly be the unprocessed <see cref="User.PasswordSHA512"/>. /// </summary> /// <param name="requestBody">Request parameters DTO wrapped into an <see cref="EpistleRequestBody"/>.</param> /// <returns>Whether deletion was successful or not.</returns> public async Task <bool> DeleteUser(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "users/delete"); RestResponse response = await restClient.ExecuteAsync(request).ConfigureAwait(false); return(response?.StatusCode == HttpStatusCode.NoContent); }
/// <summary> /// Kick a user from a conversation. /// </summary> /// <param name="requestBody">Request parameters.</param> /// <returns>Whether the user was kicked out successfully or not.</returns> public async Task <bool> KickUser(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "convos/kick", Method.Put); RestResponse response = await restClient.ExecuteAsync(request); return(response.IsSuccessful); }
/// <summary> /// Creates a new convo on the server.<para> </para> /// "<paramref name="requestBody.Body"/>" should be the <see cref="ConvoCreationRequestDto"/> serialized into JSON and compressed. /// </summary> /// <param name="requestBody">Request body containing the parameters (auth, etc...).</param> /// <returns><c>null</c> if creation failed; the created <see cref="Convo"/>'s unique id.</returns> public async Task <string> CreateConvo(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "convos/create"); RestResponse response = await restClient.ExecuteAsync(request).ConfigureAwait(false); return(response.IsSuccessful ? response.Content : null); }
/// <summary> /// Changes a convo's metadata (description, title, etc...).<para> </para> /// The user making the request needs to be the <see cref="Convo"/>'s admin (Creator).<para> </para> /// If you're assigning a new admin, he needs to be a participant of the <see cref="Convo"/>, else you'll get a bad request returned from the web api. /// </summary> /// <param name="requestBody">Request body containing the authentication parameters + the data that needs to be changed (<c>null</c> fields will be ignored; fields with values will be updated and persisted into the server's db)..</param> /// <returns>Whether the convo's metadata was changed successfully or not.</returns> public async Task <bool> ChangeConvoMetadata(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "convos/metadata", Method.Put); RestResponse response = await restClient.ExecuteAsync(request); return(response.IsSuccessful); }
/// <summary> /// Join a <see cref="Convo" />. /// </summary> /// <param name="requestBody">Request body containing the convo join parameters.</param> /// <returns>Whether the <see cref="Convo" /> was joined successfully or not.</returns> public async Task <bool> JoinConvo(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "convos/join", Method.Put); RestResponse response = await restClient.ExecuteAsync(request).ConfigureAwait(false); return(response.IsSuccessful); }
/// <summary> /// Posts a message to a <see cref="Convo" />. /// </summary> /// <param name="requestBody">Request body containing the message post parameters.</param> /// <returns>Whether the message was posted successfully or not.</returns> public async Task <bool> PostMessage(EpistleRequestBody requestBody) { var request = EpistleRequest(requestBody, "convos/post"); RestResponse response = await restClient.ExecuteAsync(request); return(response.IsSuccessful); }
#pragma warning restore 1591 /// <summary> /// Changes an Epistle <see cref="User"/>'s password. /// </summary> /// <param name="oldPw">The old <see cref="User"/> password (NOT its SHA512!).</param> /// <param name="newPw">The new, better and safer password (NOT its SHA512!).</param> /// <param name="totp">Request authentication token.</param> /// <returns>Whether the password change request was successful or not.</returns> /// <exception cref="ApplicationException">Thrown if the <see cref="User.PrivateKeyPem"/> is <c>null</c> or empty.</exception> public async Task <bool> ChangePassword(string oldPw, string newPw, string totp) { if (oldPw == newPw) { return(false); } if (user.PrivateKeyPem.NullOrEmpty()) { string msg = "The user's in-memory private key seems to be null or empty; can't change passwords without re-encrypting a new copy of the user key!"; logger?.LogError(msg); throw new ApplicationException(msg); } var dto = new UserChangePasswordRequestDto { Totp = totp, OldPwSHA512 = oldPw.SHA512(), NewPwSHA512 = newPw.SHA512(), NewPrivateKey = await keyExchange.EncryptAndCompressPrivateKeyAsync(user.PrivateKeyPem, newPw).ConfigureAwait(false) }; var requestBody = new EpistleRequestBody { UserId = user.Id, Auth = user.Token.Item2, Body = await compressionUtility.Compress(JsonSerializer.Serialize(dto)).ConfigureAwait(false) }; bool success = await userService.ChangeUserPassword(requestBody.Sign(crypto, user.PrivateKeyPem)).ConfigureAwait(false); return(success); }
/// <summary> /// Uploads a new profile picture to the server. /// </summary> /// <param name="totp">2FA Token.</param> /// <param name="pic">The profile picture (base64-encoded). Can be <c>null</c>.</param> /// <returns>Whether the update was accepted or failed.</returns> public async Task <bool> UpdateUserProfilePicture(string totp, string pic) { try { if (user is null || pic.Length > 512 * 512 || user.Id.NullOrEmpty() || user.Token is null || user.Token.Item2.NullOrEmpty() || totp.NullOrEmpty()) { return(false); } var dto = new UserChangeProfilePictureRequestDto { Totp = totp, ProfilePicture = pic, PasswordSHA512 = user.PasswordSHA512 }; var requestBody = new EpistleRequestBody { UserId = user.Id, Auth = user.Token?.Item2, Body = JsonSerializer.Serialize(dto) }; var request = EpistleRequest(requestBody.Sign(rsa, user.PrivateKeyPem), "users/pic", Method.Put); RestResponse response = await restClient.ExecuteAsync(request).ConfigureAwait(false); return(response?.StatusCode == HttpStatusCode.OK); } catch { return(false); } }
private async void TryJoinConvos() { if (!appSettings["SaveConvoPasswords", true]) { return; } var userConvos = (await userService.GetConvos(user.Id, user.Token.Item2)) .Select(c => (Convo)c) .Distinct() .Where(convo => !convo.IsExpired()).ToArray(); foreach (var convo in userConvos) { string cachedPwSHA512 = convoPasswordProvider.GetPasswordSHA512(convo.Id); if (cachedPwSHA512.NullOrEmpty()) { cachedPwSHA512 = await SecureStorage.GetAsync($"convo:{convo.Id}_pw:SHA512"); } if (cachedPwSHA512.NotNullNotEmpty()) { var dto = new ConvoJoinRequestDto { ConvoId = convo.Id, ConvoPasswordSHA512 = cachedPwSHA512 }; var body = new EpistleRequestBody { UserId = user.Id, Auth = user.Token.Item2, Body = JsonSerializer.Serialize(dto) }; if (await convoService.JoinConvo(body.Sign(crypto, user.PrivateKeyPem))) { convoPasswordProvider.SetPasswordSHA512(convo.Id, cachedPwSHA512); ConvoMetadataDto metadata = await convoService.GetConvoMetadata(convo.Id, cachedPwSHA512, user.Id, user.Token.Item2); if (metadata != null) { var viewModel = viewModelFactory.Create <ActiveConvoViewModel>(); viewModel.ActiveConvo = convo; viewModel.OnAppearing(); activeConvos[convo.Id] = viewModel; } } } } eventAggregator.GetEvent <TriggerUpdateConvosListEvent>().Publish(); }
/// <summary> /// Creates a <see cref="RestRequest"/> using the provided <paramref name="requestBody"/>, <paramref name="endpoint"/> and <paramref name="method"/>.<para> </para> /// The <paramref name="requestBody"/> is serialized to JSON and added to the HTTP request body.<para> </para> /// Please only add bodies to POST, and maybe PUT requests... /// </summary> /// <param name="requestBody">The <see cref="EpistleRequestBody"/> to serialize into JSON and add to the HTTP POST (or PUT) request's body.</param> /// <param name="endpoint">The API endpoint (relative path).</param> /// <param name="method">The HTTP method to use. Should be either POST or PUT if you use the request body.</param> /// <returns>The <see cref="RestRequest"/>, ready to be submitted.</returns> protected RestRequest EpistleRequest(EpistleRequestBody requestBody, string endpoint, Method method = Method.Post) { if (method != Method.Post && method != Method.Put) { throw new ArgumentException($"{nameof(EpistleWebApiService)}::{nameof(EpistleRequest)}: Non-PUT or POST HTTP method passed. Please only add request bodies to POST and PUT requests!"); } var request = new RestRequest( method: method, resource: new Uri(endpoint, UriKind.Relative) ); request.AddStringBody(JsonSerializer.Serialize(requestBody), "application/json"); return(request); }
/// <summary> /// Deletes a convo server-side. /// </summary> /// <param name="requestBody">Request body containing the convo deletion parameters (auth, etc...).</param> /// <returns>Whether deletion was successful or not.</returns> public async Task <bool> DeleteConvo(EpistleRequestBody requestBody) { RestResponse response = await restClient.ExecuteAsync(EpistleRequest(requestBody, "convos/delete")); return(response.IsSuccessful); }
/// <summary> /// Submits a message to a <see cref="Convo"/>. /// </summary> /// <param name="convo">The <see cref="Convo"/> to post the message into.</param> /// <param name="messageType">The type of message (e.g. "TEXT=UTF8", "FILE=example.png", etc...).</param> /// <param name="message">The message body to encrypt and post.</param> /// <returns>Whether the message could be submitted successfully or not.</returns> private async Task <bool> PostMessageToConvo(Convo convo, string messageType, byte[] message) { if (message.NullOrEmpty()) { return(false); } Task <IDictionary <string, string> > publicKeys = userService.GetUserPublicKeys(user.Id, convo.GetParticipantIdsCommaSeparated(), user.Token.Item2); byte[] compressedMessage = await compressionUtility.Compress(message, CompressionSettings.Default).ConfigureAwait(false); using var encryptionResult = await aes.EncryptAsync(compressedMessage).ConfigureAwait(false); byte[] key = new byte[48]; for (int i = 0; i < 32; i++) { key[i] = encryptionResult.Key[i]; } for (int i = 0; i < 16; i++) { key[i + 32] = encryptionResult.IV[i]; } // Encrypt the message decryption key for every convo participant individually. var encryptedKeys = new ConcurrentBag <string>(); Parallel.ForEach(await publicKeys.ConfigureAwait(false), kvp => { (string userId, string userPublicKey) = kvp; if (userId.NotNullNotEmpty() && userPublicKey.NotNullNotEmpty()) { string decompressedPublicKey = keyExchange.DecompressPublicKey(userPublicKey); encryptedKeys.Add(userId + ':' + Convert.ToBase64String(rsa.Encrypt(key, decompressedPublicKey))); } }); try { var postParamsDto = new PostMessageParamsDto { Type = messageType, SenderName = userSettings.Username, ConvoId = convo.Id, ConvoPasswordSHA512 = convoPasswordProvider.GetPasswordSHA512(convo.Id), EncryptedKeys = encryptedKeys.ToCommaSeparatedString(), EncryptedBody = Convert.ToBase64String(encryptionResult.EncryptedData) }; var body = new EpistleRequestBody { UserId = user.Id, Auth = user.Token.Item2, Body = JsonSerializer.Serialize(postParamsDto) }; return(await convoService.PostMessage(body.Sign(rsa, user.PrivateKeyPem)).ConfigureAwait(false)); } catch { return(false); } }