Clean
parent
69f05dabdf
commit
fdede1c656
@ -1,9 +1,9 @@
|
||||
package org.session.libsession.snode
|
||||
package org.session.libsignal.service.loki
|
||||
|
||||
class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||
val ip: String get() = address.removePrefix("https://")
|
||||
|
||||
internal enum class Method(val rawValue: String) {
|
||||
public enum class Method(val rawValue: String) {
|
||||
GetSwarm("get_snodes_for_pubkey"),
|
||||
GetMessages("retrieve"),
|
||||
SendMessage("store")
|
@ -1,84 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketMessage
|
||||
import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketRequestMessage
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MessageWrapper {
|
||||
|
||||
// region Types
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object FailedToWrapData : Error("Failed to wrap data.")
|
||||
object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.")
|
||||
object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.")
|
||||
object FailedToUnwrapData : Error("Failed to unwrap data.")
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Wrapping
|
||||
/**
|
||||
* Wraps `message` in a `SignalServiceProtos.Envelope` and then a `WebSocketProtos.WebSocketMessage` to match the desktop application.
|
||||
*/
|
||||
fun wrap(message: SignalMessageInfo): ByteArray {
|
||||
try {
|
||||
val envelope = createEnvelope(message)
|
||||
val webSocketMessage = createWebSocketMessage(envelope)
|
||||
return webSocketMessage.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
throw if (e is Error) { e } else { Error.FailedToWrapData }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEnvelope(message: SignalMessageInfo): Envelope {
|
||||
try {
|
||||
val builder = Envelope.newBuilder()
|
||||
builder.type = message.type
|
||||
builder.timestamp = message.timestamp
|
||||
builder.source = message.senderPublicKey
|
||||
builder.sourceDevice = message.senderDeviceID
|
||||
builder.content = ByteString.copyFrom(Base64.decode(message.content))
|
||||
return builder.build()
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to wrap message in envelope: ${e.message}.")
|
||||
throw Error.FailedToWrapMessageInEnvelope
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWebSocketMessage(envelope: Envelope): WebSocketMessage {
|
||||
try {
|
||||
val requestBuilder = WebSocketRequestMessage.newBuilder()
|
||||
requestBuilder.verb = "PUT"
|
||||
requestBuilder.path = "/api/v1/message"
|
||||
requestBuilder.id = SecureRandom.getInstance("SHA1PRNG").nextLong()
|
||||
requestBuilder.body = envelope.toByteString()
|
||||
val messageBuilder = WebSocketMessage.newBuilder()
|
||||
messageBuilder.request = requestBuilder.build()
|
||||
messageBuilder.type = WebSocketMessage.Type.REQUEST
|
||||
return messageBuilder.build()
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to wrap envelope in web socket message: ${e.message}.")
|
||||
throw Error.FailedToWrapEnvelopeInWebSocketMessage
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Unwrapping
|
||||
/**
|
||||
* `data` shouldn't be base 64 encoded.
|
||||
*/
|
||||
fun unwrap(data: ByteArray): Envelope {
|
||||
try {
|
||||
val webSocketMessage = WebSocketMessage.parseFrom(data)
|
||||
val envelopeAsData = webSocketMessage.request.body
|
||||
return Envelope.parseFrom(envelopeAsData)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to unwrap data: ${e.message}.")
|
||||
throw Error.FailedToUnwrapData
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api
|
||||
|
||||
public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||
|
||||
val ip: String get() = address.removePrefix("https://")
|
||||
|
||||
enum class Method(val rawValue: String) {
|
||||
/**
|
||||
* Only supported by snode targets.
|
||||
*/
|
||||
GetSwarm("get_snodes_for_pubkey"),
|
||||
/**
|
||||
* Only supported by snode targets.
|
||||
*/
|
||||
GetMessages("retrieve"),
|
||||
SendMessage("store")
|
||||
}
|
||||
|
||||
data class KeySet(val ed25519Key: String, val x25519Key: String)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is Snode) {
|
||||
address == other.address && port == other.port
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return address.hashCode() xor port.hashCode()
|
||||
}
|
||||
|
||||
override fun toString(): String { return "$address:$port" }
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.opengroups
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
|
||||
public data class PublicChat(
|
||||
public val channel: Long,
|
||||
private val serverURL: String,
|
||||
public val displayName: String,
|
||||
public val isDeletable: Boolean
|
||||
) {
|
||||
public val server get() = serverURL.toLowerCase()
|
||||
public val id get() = getId(channel, server)
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
||||
return "$server.$channel"
|
||||
}
|
||||
|
||||
@JvmStatic fun fromJSON(jsonAsString: String): PublicChat? {
|
||||
try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
val channel = json.get("channel").asLong()
|
||||
val server = json.get("server").asText().toLowerCase()
|
||||
val displayName = json.get("displayName").asText()
|
||||
val isDeletable = json.get("isDeletable").asBoolean()
|
||||
return PublicChat(channel, server, displayName, isDeletable)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun toJSON(): Map<String, Any> {
|
||||
return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable )
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.opengroups
|
||||
|
||||
public data class PublicChatInfo (
|
||||
public val displayName: String,
|
||||
public val profilePictureURL: String,
|
||||
public val memberCount: Int
|
||||
)
|
@ -1,178 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.opengroups
|
||||
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
|
||||
public data class PublicChatMessage(
|
||||
public val serverID: Long?,
|
||||
public val senderPublicKey: String,
|
||||
public val displayName: String,
|
||||
public val body: String,
|
||||
public val timestamp: Long,
|
||||
public val type: String,
|
||||
public val quote: Quote?,
|
||||
public val attachments: List<Attachment>,
|
||||
public val profilePicture: ProfilePicture?,
|
||||
public val signature: Signature?,
|
||||
public val serverTimestamp: Long
|
||||
) {
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
private val signatureVersion: Long = 1
|
||||
private val attachmentType = "net.app.core.oembed"
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Types
|
||||
public data class ProfilePicture(
|
||||
public val profileKey: ByteArray,
|
||||
public val url: String
|
||||
)
|
||||
|
||||
public data class Quote(
|
||||
public val quotedMessageTimestamp: Long,
|
||||
public val quoteePublicKey: String,
|
||||
public val quotedMessageBody: String,
|
||||
public val quotedMessageServerID: Long? = null
|
||||
)
|
||||
|
||||
public data class Signature(
|
||||
public val data: ByteArray,
|
||||
public val version: Long
|
||||
)
|
||||
|
||||
public data class Attachment(
|
||||
public val kind: Kind,
|
||||
public val server: String,
|
||||
public val serverID: Long,
|
||||
public val contentType: String,
|
||||
public val size: Int,
|
||||
public val fileName: String,
|
||||
public val flags: Int,
|
||||
public val width: Int,
|
||||
public val height: Int,
|
||||
public val caption: String?,
|
||||
public val url: String,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
public val linkPreviewURL: String?,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
public val linkPreviewTitle: String?
|
||||
) {
|
||||
public val dotNetAPIType = when {
|
||||
contentType.startsWith("image") -> "photo"
|
||||
contentType.startsWith("video") -> "video"
|
||||
contentType.startsWith("audio") -> "audio"
|
||||
else -> "other"
|
||||
}
|
||||
|
||||
public enum class Kind(val rawValue: String) {
|
||||
Attachment("attachment"), LinkPreview("preview")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
constructor(hexEncodedPublicKey: String, displayName: String, body: String, timestamp: Long, type: String, quote: Quote?, attachments: List<Attachment>)
|
||||
: this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments, null, null, 0)
|
||||
// endregion
|
||||
|
||||
// region Crypto
|
||||
internal fun sign(privateKey: ByteArray): PublicChatMessage? {
|
||||
val data = getValidationData(signatureVersion)
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Failed to sign public chat message.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val signatureData = curve.calculateSignature(privateKey, data)
|
||||
val signature = Signature(signatureData, signatureVersion)
|
||||
return copy(signature = signature)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to sign public chat message due to error: ${e.message}.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasValidSignature(): Boolean {
|
||||
if (signature == null) { return false }
|
||||
val data = getValidationData(signature.version) ?: return false
|
||||
val publicKey = Hex.fromStringCondensed(senderPublicKey.removing05PrefixIfNeeded())
|
||||
try {
|
||||
return curve.verifySignature(publicKey, data, signature.data)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to verify public chat message due to error: ${e.message}.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Parsing
|
||||
internal fun toJSON(): Map<String, Any> {
|
||||
val value = mutableMapOf<String, Any>( "timestamp" to timestamp )
|
||||
if (quote != null) {
|
||||
value["quote"] = mapOf( "id" to quote.quotedMessageTimestamp, "author" to quote.quoteePublicKey, "text" to quote.quotedMessageBody )
|
||||
}
|
||||
if (signature != null) {
|
||||
value["sig"] = Hex.toStringCondensed(signature.data)
|
||||
value["sigver"] = signature.version
|
||||
}
|
||||
val annotation = mapOf( "type" to type, "value" to value )
|
||||
val annotations = mutableListOf( annotation )
|
||||
attachments.forEach { attachment ->
|
||||
val attachmentValue = mutableMapOf(
|
||||
// Fields required by the .NET API
|
||||
"version" to 1,
|
||||
"type" to attachment.dotNetAPIType,
|
||||
// Custom fields
|
||||
"lokiType" to attachment.kind.rawValue,
|
||||
"server" to attachment.server,
|
||||
"id" to attachment.serverID,
|
||||
"contentType" to attachment.contentType,
|
||||
"size" to attachment.size,
|
||||
"fileName" to attachment.fileName,
|
||||
"flags" to attachment.flags,
|
||||
"width" to attachment.width,
|
||||
"height" to attachment.height,
|
||||
"url" to attachment.url
|
||||
)
|
||||
if (attachment.caption != null) { attachmentValue["caption"] = attachment.caption }
|
||||
if (attachment.linkPreviewURL != null) { attachmentValue["linkPreviewUrl"] = attachment.linkPreviewURL }
|
||||
if (attachment.linkPreviewTitle != null) { attachmentValue["linkPreviewTitle"] = attachment.linkPreviewTitle }
|
||||
val attachmentAnnotation = mapOf( "type" to attachmentType, "value" to attachmentValue )
|
||||
annotations.add(attachmentAnnotation)
|
||||
}
|
||||
val result = mutableMapOf( "text" to body, "annotations" to annotations )
|
||||
if (quote?.quotedMessageServerID != null) {
|
||||
result["reply_to"] = quote.quotedMessageServerID
|
||||
}
|
||||
return result
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private fun getValidationData(signatureVersion: Long): ByteArray? {
|
||||
var string = "${body.trim()}$timestamp"
|
||||
if (quote != null) {
|
||||
string += "${quote.quotedMessageTimestamp}${quote.quoteePublicKey}${quote.quotedMessageBody.trim()}"
|
||||
if (quote.quotedMessageServerID != null) {
|
||||
string += "${quote.quotedMessageServerID}"
|
||||
}
|
||||
}
|
||||
string += attachments.sortedBy { it.serverID }.map { it.serverID }.joinToString("")
|
||||
string += "$signatureVersion"
|
||||
try {
|
||||
return string.toByteArray(Charsets.UTF_8)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.utilities
|
||||
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal object DecryptionUtilities {
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
internal fun decryptUsingAESGCM(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray {
|
||||
val iv = ivAndCiphertext.sliceArray(0 until EncryptionUtilities.ivSize)
|
||||
val ciphertext = ivAndCiphertext.sliceArray(EncryptionUtilities.ivSize until ivAndCiphertext.count())
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(EncryptionUtilities.gcmTagSize, iv))
|
||||
return cipher.doFinal(ciphertext)
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.utilities
|
||||
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal data class EncryptionResult(
|
||||
internal val ciphertext: ByteArray,
|
||||
internal val symmetricKey: ByteArray,
|
||||
internal val ephemeralPublicKey: ByteArray
|
||||
)
|
||||
|
||||
internal object EncryptionUtilities {
|
||||
internal val gcmTagSize = 128
|
||||
internal val ivSize = 12
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
internal fun encryptUsingAESGCM(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray {
|
||||
val iv = Util.getSecretBytes(ivSize)
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
internal fun encryptForX25519PublicKey(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
|
||||
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
|
||||
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
|
||||
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey)
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
||||
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
|
||||
val ciphertext = encryptUsingAESGCM(plaintext, symmetricKey)
|
||||
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package org.session.libsignal.service.loki.database
|
||||
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
|
||||
interface LokiThreadDatabaseProtocol {
|
||||
|
||||
fun getThreadID(publicKey: String): Long
|
||||
fun getPublicChat(threadID: Long): PublicChat?
|
||||
fun setPublicChat(publicChat: PublicChat, threadID: Long)
|
||||
fun removePublicChat(threadID: Long)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.session.libsignal.service.loki.utilities
|
||||
|
||||
public object TTLUtilities {
|
||||
|
||||
/**
|
||||
* If a message type specifies an invalid TTL, this will be used.
|
||||
*/
|
||||
public val fallbackMessageTTL = 2 * 24 * 60 * 60 * 1000
|
||||
|
||||
public enum class MessageType {
|
||||
// Unimportant control messages
|
||||
Address, Call, TypingIndicator, Verified,
|
||||
// Somewhat important control messages
|
||||
DeviceLink,
|
||||
// Important control messages
|
||||
ClosedGroupUpdate, Ephemeral, SessionRequest, Receipt, Sync, DeviceUnlinkingRequest,
|
||||
// Visible messages
|
||||
Regular
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun getTTL(messageType: MessageType): Int {
|
||||
val minuteInMs = 60 * 1000
|
||||
val hourInMs = 60 * minuteInMs
|
||||
val dayInMs = 24 * hourInMs
|
||||
return when (messageType) {
|
||||
// Unimportant control messages
|
||||
MessageType.Address, MessageType.Call, MessageType.TypingIndicator, MessageType.Verified -> 20 * 1000
|
||||
// Somewhat important control messages
|
||||
MessageType.DeviceLink -> 1 * hourInMs
|
||||
// Important control messages
|
||||
MessageType.ClosedGroupUpdate, MessageType.Ephemeral, MessageType.SessionRequest, MessageType.Receipt,
|
||||
MessageType.Sync, MessageType.DeviceUnlinkingRequest -> 2 * dayInMs - 1 * hourInMs
|
||||
// Visible messages
|
||||
MessageType.Regular -> 2 * dayInMs
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue