Implement authenticated message retrieval

This commit is contained in:
Niels Andriesse 2021-07-26 11:23:58 +10:00
parent 118447799a
commit d79d236580
5 changed files with 56 additions and 56 deletions

View File

@ -8,7 +8,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.util.TypedValue
import android.view.*
import android.view.inputmethod.EditorInfo
@ -24,7 +23,6 @@ import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.PublicKeyValidation
@ -96,7 +94,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
} else {
// This could be an ONS name
showLoader()
SnodeAPI.getSessionIDFor(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
hideLoader()
this.createPrivateChat(hexEncodedPublicKey)
}.failUi { exception ->

View File

@ -104,7 +104,7 @@ class ClearAllDataDialog : BaseDialog() {
} else {
// finish
val result = try {
SnodeAPI.deleteAllMessages(requireContext()).get()
SnodeAPI.deleteAllMessages().get()
} catch (e: Exception) {
null
}

View File

@ -9,16 +9,14 @@ class MessagingModuleConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider,
val keyPairProvider: ()-> KeyPair?
val getUserED25519KeyPair: ()-> KeyPair?
) {
companion object {
lateinit var shared: MessagingModuleConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
keyPairProvider: () -> KeyPair?
fun configure(context: Context, storage: StorageProtocol,
messageDataProvider: MessageDataProvider, keyPairProvider: () -> KeyPair?
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, keyPairProvider)

View File

@ -23,8 +23,7 @@ object MessageEncrypter {
* @return the encrypted message.
*/
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
val context = MessagingModuleConfiguration.shared.context
val userED25519KeyPair = MessagingModuleConfiguration.shared.keyPairProvider() ?: throw Error.NoUserED25519KeyPair
val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey

View File

@ -2,7 +2,6 @@
package org.session.libsession.snode
import android.content.Context
import android.os.Build
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
@ -66,6 +65,8 @@ object SnodeAPI {
internal sealed class Error(val description: String) : Exception(description) {
object Generic : Error("An error occurred.")
object ClockOutOfSync : Error("Your clock is out of sync with the Service Node network.")
object NoKeyPair : Error("Missing user key pair.")
object SigningFailed : Error("Couldn't sign verification data.")
// ONS
object DecryptionFailed : Error("Couldn't decrypt ONS name.")
object HashingFailed : Error("Couldn't compute ONS name hash.")
@ -169,7 +170,7 @@ object SnodeAPI {
}
// Public API
fun getSessionIDFor(onsName: String): Promise<String, Exception> {
fun getSessionID(onsName: String): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
val promise = deferred.promise
val validationCount = 3
@ -193,7 +194,6 @@ object SnodeAPI {
retryIfNeeded(maxRetryCount) {
invoke(Snode.Method.OxenDaemonRPCCall, snode, null, parameters)
}
}
}
all(promises).success { results ->
@ -278,8 +278,27 @@ object SnodeAPI {
}
fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise {
val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair)
// Get last message hash
val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: ""
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey, "lastHash" to lastHashValue )
// Construct signature
val timestamp = Date().time + SnodeAPI.clockOffset
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
val verificationData = "retrieve$timestamp".toByteArray()
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
} catch (exception: Exception) {
return Promise.ofFail(Error.SigningFailed)
}
// Make the request
val parameters = mapOf(
"pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey,
"lastHash" to lastHashValue,
"timestamp" to timestamp,
"pubkey_ed25519" to ed25519PublicKey,
"signature" to Base64.encodeBytes(signature)
)
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
}
@ -291,7 +310,7 @@ object SnodeAPI {
}
}
fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse ->
val timestamp = rawResponse["timestamp"] as? Long ?: -1
snode to timestamp
@ -335,27 +354,26 @@ object SnodeAPI {
}
}
fun deleteAllMessages(context: Context): Promise<Map<String,Boolean>, Exception> {
fun deleteAllMessages(): Promise<Map<String,Boolean>, Exception> {
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 module = MessagingModuleConfiguration.shared
val userED25519KeyPair = module.keyPairProvider() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
getSingleTargetSnode(userPublicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val data = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes)
val verificationData = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, verificationData, verificationData.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)
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) }.fail { e ->
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map {
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
}.fail { e ->
Log.e("Loki", "Failed to clear data", e)
}
}
@ -425,37 +443,24 @@ object SnodeAPI {
@Suppress("UNCHECKED_CAST")
private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map<String, Boolean> {
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return mapOf()
val swarmResponsesValid = swarms.mapNotNull { (nodePubKeyHex, rawMap) ->
val map = rawMap as? Map<String, Any> ?: 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) {
Log.e("Loki", "Failed to delete all from $nodePubKeyHex with error code $code and reason $reason")
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
val isFailed = json["failed"] as? Boolean ?: false
val statusCode = json["code"] as? String
val reason = json["reason"] as? String
hexSnodePublicKey to if (isFailed) {
Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
false
} else {
// success
val deleted = map["deleted"] as List<String> // list of deleted hashes
val signature = map["signature"] as String
val nodePubKey = Key.fromHexString(nodePubKeyHex)
// signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
val message = (userPublicKey + timestamp.toString() + deleted.fold("") { a, v -> a + v }).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, nodePubKey.asBytes)
val hashes = json["deleted"] as List<String> // Hashes of deleted messages
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
val message = (userPublicKey + timestamp.toString() + hashes.fold("") { a, v -> a + v }).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
}
}
return swarmResponsesValid.toMap()
return result.toMap()
}
// endregion