feat: finishing up OpenGroupAPIV2.kt calls

This commit is contained in:
jubb 2021-04-19 10:16:38 +10:00
parent 96e604d06b
commit aea23a6fc1
7 changed files with 264 additions and 62 deletions

View File

@ -314,15 +314,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
override fun getLastDeletionServerId(room: String, server: String): Long? { override fun getLastDeletionServerId(room: String, server: String): Long? {
TODO("Not yet implemented") return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
} }
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) { override fun setLastDeletionServerId(room: String, server: String, newValue: Long) {
TODO("Not yet implemented") DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
} }
override fun removeLastDeletionServerId(room: String, server: String) { override fun removeLastDeletionServerId(room: String, server: String) {
TODO("Not yet implemented") DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
}
override fun setUserCount(room: String, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
} }
override fun getLastDeletionServerID(group: Long, server: String): Long? { override fun getLastDeletionServerID(group: Long, server: String): Long? {

View File

@ -316,7 +316,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
fun removeLastMessageServerID(room: String, server:String) { fun removeLastMessageServerID(room: String, server:String) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val index = "$server.$channel" val index = "$server.$room"
database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index))
} }
@ -350,6 +350,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
} }
fun removeLastDeletionServerID(room: String, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
database.delete(lastDeletionServerIDTable, "$lastDeletionServerID = ?", wrap(index))
}
fun removeLastDeletionServerID(group: Long, server: String) { fun removeLastDeletionServerID(group: Long, server: String) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val index = "$server.$group" val index = "$server.$group"
@ -379,7 +385,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
} }
override fun setUserCount(room: String, server: String, newValue: Int) { override fun setUserCount(room: String, server: String, newValue: Long) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val index = "$server.$room" val index = "$server.$room"
val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() )) val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() ))

View File

@ -78,6 +78,7 @@ interface StorageProtocol {
fun updateTitle(groupID: String, newValue: String) fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray) fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun setUserCount(room: String, server: String, newValue: Long)
// Last Message Server ID // Last Message Server ID
fun getLastMessageServerId(room: String, server: String): Long? fun getLastMessageServerId(room: String, server: String): Long?

View File

@ -1,19 +1,36 @@
package org.session.libsession.messaging.opengroups package org.session.libsession.messaging.opengroups
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM
import org.session.libsignal.service.loki.api.utilities.HTTP import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.api.utilities.HTTP.Verb.*
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Base64.*
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.createContext
import org.session.libsignal.utilities.logging.Log
import org.whispersystems.curve25519.Curve25519
import java.util.* import java.util.*
object OpenGroupAPIV2 { object OpenGroupAPIV2 {
private val moderators: HashMap<String, HashMap<String, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
const val DEFAULT_SERVER = "https://sessionopengroup.com" const val DEFAULT_SERVER = "https://sessionopengroup.com"
const val DEFAULT_SERVER_PUBLIC_KEY = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b" const val DEFAULT_SERVER_PUBLIC_KEY = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b"
private val sharedContext = Kovenant.createContext()
private val curve = Curve25519.getInstance(Curve25519.BEST)
sealed class Error : Exception() { sealed class Error : Exception() {
object GENERIC : Error() object GENERIC : Error()
object PARSING_FAILED : Error() object PARSING_FAILED : Error()
@ -26,7 +43,7 @@ object OpenGroupAPIV2 {
data class Info( data class Info(
val id: String, val id: String,
val name: String, val name: String,
val imageID: String val imageID: String?
) )
data class Request( data class Request(
@ -34,30 +51,67 @@ object OpenGroupAPIV2 {
val room: String?, val room: String?,
val server: String, val server: String,
val endpoint: String, val endpoint: String,
val queryParameters: Map<String, String>, val queryParameters: Map<String, String> = mapOf(),
val parameters: Any, val parameters: Any? = null,
val headers: Map<String, String>, val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean, val isAuthRequired: Boolean = true,
// Always `true` under normal circumstances. You might want to disable // Always `true` under normal circumstances. You might want to disable
// this when running over Lokinet. // this when running over Lokinet.
val useOnionRouting: Boolean val useOnionRouting: Boolean = true
) )
private fun send(request: Request): Promise<Any, Exception> { private fun createBody(parameters: Any): RequestBody {
val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
}
private fun send(request: Request): Promise<Map<*,*>, Exception> {
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL) val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(parsed.scheme()) .scheme(parsed.scheme())
.host(parsed.host()) .host(parsed.host())
.addPathSegment(request.endpoint) .addPathSegment(request.endpoint)
for ((key, value) in request.queryParameters) { if (request.verb == GET) {
urlBuilder.addQueryParameter(key, value) for ((key, value) in request.queryParameters) {
urlBuilder.addQueryParameter(key, value)
}
} }
fun execute(token: String?): Promise<Map<*, *>, Exception> { fun execute(token: String?): Promise<Map<*, *>, Exception> {
val requestBuilder = okhttp3.Request.Builder()
.url(urlBuilder.build())
if (request.isAuthRequired) {
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request")
requestBuilder.addHeader("Authorization", token)
}
when (request.verb) {
GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.parameters!!))
POST -> requestBuilder.post(createBody(request.parameters!!))
DELETE -> requestBuilder.delete(createBody(request.parameters!!))
}
if (!request.room.isNullOrEmpty()) {
requestBuilder.header("Room", request.room)
}
if (request.useOnionRouting) {
val publicKey = MessagingConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey)
.fail { e ->
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException
&& e.statusCode == 401) {
MessagingConfiguration.shared.storage.removeAuthToken(request.server)
}
}
} else {
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
}
} }
return if (request.isAuthRequired) { return if (request.isAuthRequired) {
getAuthToken(request.room!!, request.server).bind(::execute) getAuthToken(request.room!!, request.server).bind(sharedContext) { execute(it) }
} else { } else {
execute(null) execute(null)
} }
@ -69,7 +123,7 @@ object OpenGroupAPIV2 {
Promise.of(it) Promise.of(it)
} ?: run { } ?: run {
requestNewAuthToken(room, server) requestNewAuthToken(room, server)
.bind { claimAuthToken(it, room, server) } .bind(sharedContext) { claimAuthToken(it, room, server) }
.success { authToken -> .success { authToken ->
storage.setAuthToken(room, server, authToken) storage.setAuthToken(room, server, authToken)
} }
@ -77,75 +131,204 @@ object OpenGroupAPIV2 {
} }
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> { fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
val (publicKey, _) = MessagingConfiguration.shared.storage.getUserKeyPair() val (publicKey, privateKey) = MessagingConfiguration.shared.storage.getUserKeyPair()
?: return Promise.ofFail(Error.GENERIC) ?: return Promise.ofFail(Error.GENERIC)
val queryParameters = mutableMapOf("public_key" to publicKey) val queryParameters = mutableMapOf("public_key" to publicKey)
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
return send(request).map(sharedContext) { json ->
val challenge = json["challenge"] as? Map<*,*> ?: throw Error.PARSING_FAILED
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.PARSING_FAILED
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.PARSING_FAILED
val ciphertext = decode(base64EncodedCiphertext)
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
val tokenAsData = try {
AESGCM.decrypt(ciphertext, symmetricKey)
} catch (e: Exception) {
throw Error.DECRYPTION_FAILED
}
tokenAsData.toHexString()
}
} }
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> { fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
TODO("implement") val parameters = mapOf("public_key" to MessagingConfiguration.shared.storage.getUserPublicKey()!!)
val headers = mapOf("Authorization" to authToken)
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
parameters = parameters, headers = headers, isAuthRequired = false)
return send(request).map(sharedContext) { authToken }
} }
fun deleteAuthToken(room: String, server: String): Promise<Long, Exception> { fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> {
TODO("implement") val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token")
return send(request).map(sharedContext) {
MessagingConfiguration.shared.storage.removeAuthToken(room, server)
}
} }
// region Sending
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> { fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
TODO("implement") val base64EncodedFile = encodeBytes(file)
val parameters = mapOf("file" to base64EncodedFile)
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
return send(request).map(sharedContext) { json ->
json["result"] as? Long ?: throw Error.PARSING_FAILED
}
} }
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> { fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
TODO("implement") val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
return send(request).map(sharedContext) { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
}
} }
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> { fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
TODO("implement") val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
val json = signedMessage.toJSON()
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = json)
return send(request).map(sharedContext) {
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String,String> ?: throw Error.PARSING_FAILED
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
}
} }
// endregion
// region Messages
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> { fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
TODO("implement") val storage = MessagingConfiguration.shared.storage
} val queryParameters = mutableMapOf<String,String>()
storage.getLastMessageServerId(room,server)?.let { lastId ->
queryParameters += "from_server_id" to lastId.toString()
}
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
return send(request).map(sharedContext) { jsonList ->
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String,Any>> ?: throw Error.PARSING_FAILED
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
var currentMax = lastMessageServerId
val messages = rawMessages.mapNotNull { json ->
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
val sender = message.sender
val data = decode(message.base64EncodedData)
val signature = decode(message.base64EncodedSignature)
val publicKey = sender.removing05PrefixIfNeeded().encodeToByteArray()
val isValid = curve.verifySignature(publicKey, data, signature)
if (!isValid) {
Log.d("Loki", "Ignoring message with invalid signature")
return@mapNotNull null
}
if (message.serverID > lastMessageServerId) {
currentMax = message.serverID
}
message
}
storage.setLastMessageServerId(room,server,currentMax)
messages
}
}
// endregion
// region Message Deletion
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> { fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
TODO("implement") val request = Request(verb = DELETE, room = room, server = server, endpoint = "message/$serverID")
return send(request).map(sharedContext) {
Log.d("Loki", "Deleted server message")
}
} }
fun getDeletedMessages(room: String, server: String): Promise<List<Long>, Exception> { fun getDeletedMessages(room: String, server: String): Promise<List<Long>, Exception> {
TODO("implement") val storage = MessagingConfiguration.shared.storage
val queryParameters = mutableMapOf<String,String>()
storage.getLastDeletionServerId(room, server)?.let { last ->
queryParameters["from_server_id"] = last.toString()
}
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
return send(request).map(sharedContext) { json ->
@Suppress("UNCHECKED_CAST") val serverIDs = json["ids"] as? List<Long> ?: throw Error.PARSING_FAILED
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
val serverID = serverIDs.maxOrNull() ?: 0
if (serverID > lastMessageServerId) {
storage.setLastDeletionServerId(room, server, serverID)
}
serverIDs
}
} }
// endregion
// region Moderation
fun getModerators(room: String, server: String): Promise<List<String>, Exception> { fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
TODO("implement") val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
return send(request).map(sharedContext) { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String> ?: throw Error.PARSING_FAILED
val id = "$server.$room"
moderators[id] = moderatorsJson.toMutableSet()
moderatorsJson
}
} }
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
TODO("implement") val parameters = mapOf("public_key" to publicKey)
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
return send(request).map(sharedContext) {
Log.d("Loki", "Banned user $publicKey from $server.$room")
}
} }
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
TODO("implement") val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
return send(request).map(sharedContext) {
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
}
} }
fun isUserModerator(publicKey: String, room: String, server: String): Promise<Boolean, Exception> { fun isUserModerator(publicKey: String, room: String, server: String): Boolean = moderators["$server.$room"]?.contains(publicKey) ?: false
TODO("implement") // endregion
}
fun getDefaultRoomsIfNeeded() { // region General
TODO("implement") fun getDefaultRoomsIfNeeded(): Promise<List<Info>, Exception> {
val storage = MessagingConfiguration.shared.storage
storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
return getAllRooms(DEFAULT_SERVER)
} }
fun getInfo(room: String, server: String): Promise<Info, Exception> { fun getInfo(room: String, server: String): Promise<Info, Exception> {
TODO("implement") val request = Request(verb = GET, room = room, server = server, endpoint = "rooms/$room", isAuthRequired = false)
return send(request).map(sharedContext) { json ->
val rawRoom = json["room"] as? Map<*,*> ?: throw Error.PARSING_FAILED
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
val imageID = rawRoom["image_id"] as? String
Info(id = id, name = name, imageID = imageID)
}
} }
fun getAllRooms(server: String): Promise<List<Info>, Exception> { fun getAllRooms(server: String): Promise<List<Info>, Exception> {
TODO("implement") val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
return send(request).map(sharedContext) { json ->
val rawRooms = json["rooms"] as? Map<*,*> ?: throw Error.PARSING_FAILED
rawRooms.mapNotNull {
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
val id = roomJson["id"] as? String ?: return@mapNotNull null
val name = roomJson["name"] as? String ?: return@mapNotNull null
val imageId = roomJson["image_id"] as? String
Info(id, name, imageId)
}
}
} }
fun getMemberCount(room: String, server: String): Promise<Long, Exception> { fun getMemberCount(room: String, server: String): Promise<Long, Exception> {
TODO("implement") val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
return send(request).map(sharedContext) { json ->
val memberCount = json["member_count"] as? Long ?: throw Error.PARSING_FAILED
val storage = MessagingConfiguration.shared.storage
storage.setUserCount(room, server, memberCount)
memberCount
}
} }
// endregion
} }

View File

@ -18,6 +18,21 @@ data class OpenGroupMessageV2(
companion object { companion object {
private val curve = Curve25519.getInstance(Curve25519.BEST) private val curve = Curve25519.getInstance(Curve25519.BEST)
fun fromJSON(json: Map<String, Any>): OpenGroupMessageV2? {
val base64EncodedData = json["data"] as? String ?: return null
val sentTimestamp = json["timestamp"] as? Long ?: return null
val serverID = json["server_id"] as? Long
val sender = json["public_key"] as? String
val base64EncodedSignature = json["signature"] as? String
return OpenGroupMessageV2(serverID = serverID,
sender = sender,
sentTimestamp = sentTimestamp,
base64EncodedData = base64EncodedData,
base64EncodedSignature = base64EncodedSignature
)
}
} }
fun sign(): OpenGroupMessageV2? { fun sign(): OpenGroupMessageV2? {
@ -43,20 +58,4 @@ data class OpenGroupMessageV2(
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
return jsonMap return jsonMap
} }
fun fromJSON(json: Map<String, Any>): OpenGroupMessageV2? {
val base64EncodedData = json["data"] as? String ?: return null
val sentTimestamp = json["timestamp"] as? Long ?: return null
val serverID = json["server_id"] as? Long
val sender = json["public_key"] as? String
val base64EncodedSignature = json["signature"] as? String
return OpenGroupMessageV2(serverID = serverID,
sender = sender,
sentTimestamp = sentTimestamp,
base64EncodedData = base64EncodedData,
base64EncodedSignature = base64EncodedSignature
)
}
} }

View File

@ -1,14 +1,16 @@
package org.session.libsession.utilities package org.session.libsession.utilities
import org.whispersystems.curve25519.Curve25519 import androidx.annotation.WorkerThread
import org.session.libsignal.libsignal.util.ByteUtil import org.session.libsignal.libsignal.util.ByteUtil
import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.whispersystems.curve25519.Curve25519
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@WorkerThread
internal object AESGCM { internal object AESGCM {
internal data class EncryptionResult( internal data class EncryptionResult(
@ -31,6 +33,16 @@ internal object AESGCM {
return cipher.doFinal(ciphertext) return cipher.doFinal(ciphertext)
} }
/**
* Sync. Don't call from the main thread.
*/
internal fun generateSymmetricKey(x25519PublicKey: ByteArray, x25519PrivateKey: ByteArray): ByteArray {
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, x25519PrivateKey)
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
return mac.doFinal(ephemeralSharedSecret)
}
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */
@ -47,10 +59,7 @@ internal object AESGCM {
internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult { internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey) val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair() val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey) val symmetricKey = generateSymmetricKey(x25519PublicKey, ephemeralKeyPair.privateKey)
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
val ciphertext = encrypt(plaintext, symmetricKey) val ciphertext = encrypt(plaintext, symmetricKey)
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey) return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
} }

View File

@ -28,7 +28,7 @@ interface LokiAPIDatabaseProtocol {
fun setLastMessageServerID(room: String, server: String, newValue: Long) fun setLastMessageServerID(room: String, server: String, newValue: Long)
fun getLastDeletionServerID(room: String, server: String): Long? fun getLastDeletionServerID(room: String, server: String): Long?
fun setLastDeletionServerID(room: String, server: String, newValue: Long) fun setLastDeletionServerID(room: String, server: String, newValue: Long)
fun setUserCount(room: String, server: String, newValue: Int) fun setUserCount(room: String, server: String, newValue: Long)
fun getSessionRequestSentTimestamp(publicKey: String): Long? fun getSessionRequestSentTimestamp(publicKey: String): Long?
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
fun getSessionRequestProcessedTimestamp(publicKey: String): Long? fun getSessionRequestProcessedTimestamp(publicKey: String): Long?