Merge pull request #676 from oxen-io/authentication

Implement Authenticated Message Retrieval
This commit is contained in:
Niels Andriesse 2021-07-26 11:25:12 +10:00 committed by GitHub
commit ea5a41af52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.* import android.view.*
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
@ -24,7 +23,6 @@ import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
@ -96,7 +94,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
} else { } else {
// This could be an ONS name // This could be an ONS name
showLoader() showLoader()
SnodeAPI.getSessionIDFor(onsNameOrPublicKey).successUi { hexEncodedPublicKey -> SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
hideLoader() hideLoader()
this.createPrivateChat(hexEncodedPublicKey) this.createPrivateChat(hexEncodedPublicKey)
}.failUi { exception -> }.failUi { exception ->

View File

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

View File

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

View File

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

View File

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