mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 13:38:26 +00:00
Merge pull request #676 from oxen-io/authentication
Implement Authenticated Message Retrieval
This commit is contained in:
commit
ea5a41af52
@ -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 ->
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user