diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index 2e34f83b79..12866caa4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -76,30 +76,37 @@ class ClearAllDataDialog(val deleteNetworkMessages: Boolean) : DialogFragment() } } catch (e: Exception) { Log.e("Loki", "Failed to force sync", e) + withContext(Dispatchers.Main) { + updateUI(false) + } } } else { // finish - val promises = SnodeAPI.deleteAllMessages(requireContext()).get() + val promises = try { + SnodeAPI.deleteAllMessages(requireContext()).get() + } catch (e: Exception) { + null + } - val rawResponses = promises.map { + val rawResponses = promises?.map { try { it.get() } catch (e: Exception) { null } - } + } ?: listOf(null) // TODO: process the responses here - if (rawResponses.any { it == null || it["failed"] as? Boolean == true }) { - // didn't succeed (at least one) - withContext(Dispatchers.Main) { - updateUI(false) - } - } else { + if (rawResponses.all { it != null }) { // don't force sync because all the messages are deleted? ApplicationContext.getInstance(context).clearAllData(false) withContext(Dispatchers.Main) { dismiss() } + } else if (rawResponses.any { it == null || it["failed"] as? Boolean == true }) { + // didn't succeed (at least one) + withContext(Dispatchers.Main) { + updateUI(false) + } } } } diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index ba8d17804b..ea38f9475e 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -312,41 +312,35 @@ object SnodeAPI { } } - /** Deletes all messages owned by the given pubkey on this SN and broadcasts the delete request to - * all other swarm members. - * Returns dict of: - * - "swarms" dict mapping ed25519 pubkeys (in hex) of swarm members to dict values of: - * - "failed" and other failure keys -- see `recursive`. - * - "deleted": hashes of deleted messages. - * - "signature": signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ), signed - * by the node's ed25519 pubkey. - */ fun deleteAllMessages(context: Context): Promise, Exception> { - return retryIfNeeded(1) { + return retryIfNeeded(maxRetryCount) { // considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous - val ed = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic) - val xPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey - val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair() + val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic) val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic) val destination = if (useTestnet) userPublicKey.removing05PrefixIfNeeded() else userPublicKey - getTargetSnodes(destination).map { swarm -> - swarm.map { snode -> - retryIfNeeded(1) { + getSwarm(destination).map { swarm -> + val promise = swarm.first().let { snode -> + retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> val signature = ByteArray(Sign.BYTES) - val data = Snode.Method.DeleteAll.rawValue.toByteArray() + timestamp.toString().toByteArray() - val signed = sodium.cryptoSignDetached(signature, data, data.size.toLong(), xPrivateKey.serialize()) - val deleteMessage = SnodeDeleteMessage(userPublicKey, timestamp, Base64.encodeBytes(signature)) - val parameters = deleteMessage.toJSON() - invoke(Snode.Method.DeleteAll, snode, destination, parameters).fail { e -> + val data = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray() + sodium.cryptoSignDetached(signature, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes) + val deleteMessageParams = mapOf( + "pubkey" to userPublicKey, + "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, + "timestamp" to timestamp, + "signature" to Base64.encodeBytes(signature) + ) + invoke(Snode.Method.DeleteAll, snode, destination, deleteMessageParams).map { rawResponse -> parseDeletions(timestamp, rawResponse) }.fail { e -> Log.e("Loki", "Failed to clear data",e) } } } - }.toSet() + } + setOf(promise) } } } @@ -433,6 +427,43 @@ object SnodeAPI { } } } + + @Suppress("UNCHECKED_CAST") + private fun parseDeletions(timestamp: Long, rawResponse: RawResponse): Map { + val swarms = rawResponse["swarms"] as? Map ?: return mapOf() + val swarmResponsesValid = swarms.mapNotNull { (nodePubKeyHex, rawMap) -> + val map = rawMap as? Map ?: return@mapNotNull null + /** Deletes all messages owned by the given pubkey on this SN and broadcasts the delete request to + * all other swarm members. + * Returns dict of: + * - "swarms" dict mapping ed25519 pubkeys (in hex) of swarm members to dict values of: + * - "failed" and other failure keys -- see `recursive`. + * - "deleted": hashes of deleted messages. + * - "signature": signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ), signed + * by the node's ed25519 pubkey. + */ + // failure + val failed = map["failed"] as? Boolean ?: false + val code = map["code"] as? String + val reason = map["reason"] as? String + + nodePubKeyHex to if (failed) { + // return error probs + false + } else { + // success + val deleted = map["deleted"] as List // list of deleted hashes + Log.d("Loki", "node $nodePubKeyHex deleted ${deleted.size} messages") + val signature = map["signature"] as String + val nodePubKeyBytes = Hex.fromStringCondensed(nodePubKeyHex) + // signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) + val message = (signature + timestamp + deleted.fold("") { a, v -> a+v }).toByteArray() + sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, nodePubKeyBytes) + } + } + return swarmResponsesValid.toMap() + } + // endregion // Error Handling diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt deleted file mode 100644 index aa98a93de1..0000000000 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeDeleteMessage.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.session.libsession.snode - -import org.session.libsignal.utilities.removing05PrefixIfNeeded - -data class SnodeDeleteMessage( - /** - * The hex encoded public key of the user. - */ - val pubKey: String, - /** - * The timestamp at which this request was initiated, in milliseconds since unix epoch. - * Must be within Must be within ±60s of the current time. - * (For clients it is recommended to retrieve a timestamp via `info` first, to avoid client time sync issues). - */ - val timestamp: Long, - /** - * a Base64-encoded signature of ( "delete_all" || timestamp ), signed by the pubKey - */ - val signature: String, -) { - - internal fun toJSON(): Map { - return mapOf( - "pubkey" to pubKey, - "timestamp" to timestamp, - "signature" to signature - ) - } - -} \ No newline at end of file