Merge remote-tracking branch 'upstream/dev' into origin/refactor-sending

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
#	app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt
#	libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt
#	libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
#	libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt
This commit is contained in:
jubb
2021-03-18 09:26:13 +11:00
97 changed files with 1592 additions and 3377 deletions

View File

@@ -38,7 +38,7 @@ dependencies {
// Remote:
implementation 'org.greenrobot:eventbus:3.0.0'
implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
@@ -61,10 +61,10 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.threeten:threetenbp:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation "junit:junit:3.8.2"

View File

@@ -58,6 +58,8 @@ interface StorageProtocol {
fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, PublicChat>
fun addOpenGroup(server: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?
@@ -94,7 +96,6 @@ interface StorageProtocol {
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
@@ -112,9 +113,9 @@ interface StorageProtocol {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>)
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long)
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?

View File

@@ -1,40 +0,0 @@
package org.session.libsession.messaging.avatars;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.color.MaterialColor;
/**
* Used for migrating legacy colors to modern colors. For normal color generation, use
* {@link ContactColors}.
*/
public class ContactColorsLegacy {
private static final String[] LEGACY_PALETTE = new String[] {
"red",
"pink",
"purple",
"deep_purple",
"indigo",
"blue",
"light_blue",
"cyan",
"teal",
"green",
"light_green",
"orange",
"deep_orange",
"amber",
"blue_grey"
};
public static MaterialColor generateFor(@NonNull String name) {
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
try {
return MaterialColor.fromSerialized(serialized);
} catch (MaterialColor.UnknownColorException e) {
return ContactColors.generateFor(name);
}
}
}

View File

@@ -83,14 +83,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
//serialize Message and Destination properties
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val serializedDestination = ByteArray(4096)
var output = Output(serializedMessage)
val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
kryo.writeClassAndObject(output, message)
output.close()
output = Output(serializedDestination)
val serializedMessage = output.toBytes()
output.clear()
kryo.writeClassAndObject(output, destination)
output.close()
val serializedDestination = output.toBytes()
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
.putByteArray(KEY_DESTINATION, serializedDestination)
.build();

View File

@@ -7,25 +7,14 @@ import org.session.libsignal.service.loki.utilities.toHexString
sealed class Destination {
class Contact() : Destination() {
var publicKey: String = ""
internal constructor(publicKey: String): this() {
this.publicKey = publicKey
}
class Contact(var publicKey: String) : Destination() {
internal constructor(): this("")
}
class ClosedGroup() : Destination() {
var groupPublicKey: String = ""
internal constructor(groupPublicKey: String): this() {
this.groupPublicKey = groupPublicKey
}
class ClosedGroup(var groupPublicKey: String) : Destination() {
internal constructor(): this("")
}
class OpenGroup() : Destination() {
var channel: Long = 0
var server: String = ""
internal constructor(channel: Long, server: String): this() {
this.channel = channel
this.server = server
}
class OpenGroup(var channel: Long, var server: String) : Destination() {
internal constructor(): this(0, "")
}
companion object {

View File

@@ -1,5 +1,7 @@
package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
abstract class Message {
@@ -29,4 +31,12 @@ abstract class Message {
abstract fun toProto(): SignalServiceProtos.Content?
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
dataMessage.group = groupProto.build()
}
}

View File

@@ -1,6 +1,10 @@
package org.session.libsession.messaging.messages.control
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
@@ -26,18 +30,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
// Kind enum
sealed class Kind {
class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<ByteString>, val admins: List<ByteString>) : Kind()
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
}
/// - Note: Deprecated in favor of more explicit group updates.
class Update(val name: String, val members: List<ByteString>) : Kind()
class Update(var name: String, var members: List<ByteString>) : Kind() {
internal constructor(): this("", listOf())
}
/// An encryption key pair encrypted for each member individually.
///
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection<KeyPairWrapper>) : Kind()
class NameChange(val name: String) : Kind()
class MembersAdded(val members: List<ByteString>) : Kind()
class MembersRemoved( val members: List<ByteString>) : Kind()
class MemberLeft : Kind()
class EncryptionKeyPairRequest: Kind()
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
internal constructor(): this(null, listOf())
}
class NameChange(var name: String) : Kind() {
internal constructor(): this("")
}
class MembersAdded(var members: List<ByteString>) : Kind() {
internal constructor(): this(listOf())
}
class MembersRemoved(var members: List<ByteString>) : Kind() {
internal constructor(): this(listOf())
}
class MemberLeft() : Kind()
class EncryptionKeyPairRequest(): Kind()
val description: String = run {
when(this) {
@@ -115,8 +131,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
val kind = kind ?: return false
return when(kind) {
is Kind.New -> {
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair.publicKey != null
&& kind.encryptionKeyPair.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
}
is Kind.Update -> kind.name.isNotEmpty()
is Kind.EncryptionKeyPair -> true
@@ -141,16 +157,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupControlMessage.publicKey = kind.publicKey
closedGroupControlMessage.name = kind.name
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
try {
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
return null
}
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
closedGroupControlMessage.addAllMembers(kind.members)
closedGroupControlMessage.addAllAdmins(kind.admins)
}
@@ -187,6 +197,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
val dataMessageProto = DataMessage.newBuilder()
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
// Group context
setGroupContext(dataMessageProto)
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
contentProto.dataMessage = dataMessageProto.build()
return contentProto.build()
} catch (e: Exception) {

View File

@@ -14,11 +14,13 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>, val contacts: List<Contact>, val displayName: String, val profilePicture: String?, val profileKey: ByteArray): ControlMessage() {
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
internal constructor(): this("", "", null, listOf(), listOf())
override fun toString(): String {
return name
}
@@ -30,7 +32,7 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
val name = proto.name
val encryptionKeyPairAsProto = proto.encryptionKeyPair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
val members = proto.membersList.map { it.toByteArray().toHexString() }
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
@@ -42,8 +44,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.name = name
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
@@ -51,7 +53,10 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
}
}
class Contact(val publicKey: String, val name: String, val profilePicture: String?, val profileKey: ByteArray?) {
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
internal constructor(): this("", "", null, null)
companion object {
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
if (!proto.hasName() || !proto.hasProfileKey()) return null
@@ -123,10 +128,13 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
val displayName = configurationProto.displayName
val profilePicture = configurationProto.profilePicture
val profileKey = configurationProto.profileKey
return ConfigurationMessage(closedGroups, openGroups, listOf(), displayName, profilePicture, profileKey.toByteArray())
val contacts = configurationProto.contactsList.mapNotNull { Contact.fromProto(it) }
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey.toByteArray())
}
}
internal constructor(): this(listOf(), listOf(), listOf(), "", null, byteArrayOf())
override fun toProto(): SignalServiceProtos.Content? {
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })

View File

@@ -1,7 +1,9 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
class ExpirationTimerUpdate() : ControlMessage() {
@@ -40,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() {
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
dataMessageProto.expireTimer = duration
syncTarget?.let { dataMessageProto.syncTarget = it }
// Group context
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
try {
setGroupContext(dataMessageProto)
} catch(e: Exception) {
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
return null
}
}
val contentProto = SignalServiceProtos.Content.newBuilder()
try {
contentProto.dataMessage = dataMessageProto.build()

View File

@@ -0,0 +1,134 @@
package org.session.libsession.messaging.messages.signal;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class IncomingMediaMessage {
private final Address from;
private final Address groupId;
private final String body;
private final boolean push;
private final long sentTimeMillis;
private final int subscriptionId;
private final long expiresIn;
private final boolean expirationUpdate;
private final QuoteModel quote;
private final boolean unidentified;
private final List<Attachment> attachments = new LinkedList<>();
private final List<Contact> sharedContacts = new LinkedList<>();
private final List<LinkPreview> linkPreviews = new LinkedList<>();
public IncomingMediaMessage(Address from,
long sentTimeMillis,
int subscriptionId,
long expiresIn,
boolean expirationUpdate,
boolean unidentified,
Optional<String> body,
Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments,
Optional<QuoteModel> quote,
Optional<List<Contact>> sharedContacts,
Optional<List<LinkPreview>> linkPreviews)
{
this.push = true;
this.from = from;
this.sentTimeMillis = sentTimeMillis;
this.body = body.orNull();
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
this.quote = quote.orNull();
this.unidentified = unidentified;
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
else this.groupId = null;
this.attachments.addAll(PointerAttachment.forPointers(attachments));
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
}
public static IncomingMediaMessage from(VisibleMessage message,
Address from,
long expiresIn,
Optional<SignalServiceGroup> group,
List<SignalServiceAttachment> attachments,
Optional<QuoteModel> quote,
Optional<List<LinkPreview>> linkPreviews)
{
return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false,
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews);
}
public int getSubscriptionId() {
return subscriptionId;
}
public String getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments;
}
public Address getFrom() {
return from;
}
public Address getGroupId() {
return groupId;
}
public boolean isPushMessage() {
return push;
}
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
public long getExpiresIn() {
return expiresIn;
}
public boolean isGroupMessage() {
return groupId != null;
}
public QuoteModel getQuote() {
return quote;
}
public List<Contact> getSharedContacts() {
return sharedContacts;
}
public List<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
public boolean isUnidentified() {
return unidentified;
}
}

View File

@@ -0,0 +1,29 @@
package org.session.libsession.messaging.messages.signal;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.LinkedList;
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
Collections.emptyList());
}
public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message,
Recipient recipient) {
return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000);
}
@Override
public boolean isExpirationUpdate() {
return true;
}
}

View File

@@ -0,0 +1,89 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsignal.utilities.Base64;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
private final GroupContext group;
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull String encodedGroupContext,
@NonNull List<Attachment> avatar,
long sentTimeMillis,
long expiresIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
throws IOException
{
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long expireIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group;
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long sentTime,
long expireIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
sentTime,
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group;
}
@Override
public boolean isGroup() {
return true;
}
public boolean isGroupUpdate() {
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
}
public boolean isGroupQuit() {
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
}
public GroupContext getGroupContext() {
return group;
}
}

View File

@@ -0,0 +1,140 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class OutgoingMediaMessage {
private final Recipient recipient;
protected final String body;
protected final List<Attachment> attachments;
private final long sentTimeMillis;
private final int distributionType;
private final int subscriptionId;
private final long expiresIn;
private final QuoteModel outgoingQuote;
private final List<NetworkFailure> networkFailures = new LinkedList<>();
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
private final List<Contact> contacts = new LinkedList<>();
private final List<LinkPreview> linkPreviews = new LinkedList<>();
public OutgoingMediaMessage(Recipient recipient, String message,
List<Attachment> attachments, long sentTimeMillis,
int subscriptionId, long expiresIn,
int distributionType,
@Nullable QuoteModel outgoingQuote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
@NonNull List<NetworkFailure> networkFailures,
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
{
this.recipient = recipient;
this.body = message;
this.sentTimeMillis = sentTimeMillis;
this.distributionType = distributionType;
this.attachments = attachments;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.outgoingQuote = outgoingQuote;
this.contacts.addAll(contacts);
this.linkPreviews.addAll(linkPreviews);
this.networkFailures.addAll(networkFailures);
this.identityKeyMismatches.addAll(identityKeyMismatches);
}
public OutgoingMediaMessage(OutgoingMediaMessage that) {
this.recipient = that.getRecipient();
this.body = that.body;
this.distributionType = that.distributionType;
this.attachments = that.attachments;
this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId;
this.expiresIn = that.expiresIn;
this.outgoingQuote = that.outgoingQuote;
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
this.networkFailures.addAll(that.networkFailures);
this.contacts.addAll(that.contacts);
this.linkPreviews.addAll(that.linkPreviews);
}
public static OutgoingMediaMessage from(VisibleMessage message,
Recipient recipient,
List<Attachment> attachments,
@Nullable QuoteModel outgoingQuote,
@Nullable LinkPreview linkPreview)
{
List<LinkPreview> previews = Collections.emptyList();
if (linkPreview != null) {
previews = Collections.singletonList(linkPreview);
}
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
previews, Collections.emptyList(), Collections.emptyList());
}
public Recipient getRecipient() {
return recipient;
}
public String getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments;
}
public boolean isSecure() {
return true;
}
public boolean isGroup() {
return false;
}
public boolean isExpirationUpdate() {
return false;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresIn;
}
public @Nullable QuoteModel getOutgoingQuote() {
return outgoingQuote;
}
public @NonNull List<Contact> getSharedContacts() {
return contacts;
}
public @NonNull List<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
}

View File

@@ -0,0 +1,37 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.List;
public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
public OutgoingSecureMediaMessage(Recipient recipient, String body,
List<Attachment> attachments,
long sentTimeMillis,
int distributionType,
long expiresIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
super(base);
}
@Override
public boolean isSecure() {
return true;
}
}

View File

@@ -4,6 +4,9 @@ import com.goterl.lazycode.lazysodium.BuildConfig
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.logging.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
@@ -102,11 +105,28 @@ class VisibleMessage : Message() {
}
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
dataMessage.addAllAttachments(attachmentPointers)
// TODO Contact
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
val storage = MessagingConfiguration.shared.storage
val context = MessagingConfiguration.shared.context
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
dataMessage.expireTimer = expiration
// Group context
if (storage.isClosedGroup(recipient!!)) {
try {
setGroupContext(dataMessage)
} catch(e: Exception) {
Log.w(TAG, "Couldn't construct visible message proto from: $this")
return null
}
}
// Sync target
if (syncTarget != null) {
dataMessage.syncTarget = syncTarget
}
// TODO Contact
// Build
try {
proto.dataMessage = dataMessage.build()

View File

@@ -41,6 +41,7 @@ object OpenGroupAPI: DotNetAPI() {
return listOf() // Don't auto-join any open groups right now
}
@JvmStatic
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
if (moderators[server] != null && moderators[server]!![channel] != null) {
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
@@ -113,7 +114,7 @@ object OpenGroupAPI: DotNetAPI() {
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
val contentType = attachmentAsJSON["contentType"] as String
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
val fileName = attachmentAsJSON["fileName"] as String
val fileName = attachmentAsJSON["fileName"] as? String
val flags = 0
val url = attachmentAsJSON["url"] as String
val caption = attachmentAsJSON["caption"] as? String
@@ -237,6 +238,7 @@ object OpenGroupAPI: DotNetAPI() {
}
}
@JvmStatic
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
return retryIfNeeded(maxRetryCount) {
val isModerationRequest = !isSentByUser
@@ -337,6 +339,7 @@ object OpenGroupAPI: DotNetAPI() {
}
}
@JvmStatic
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
return retryIfNeeded(maxRetryCount) {
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {

View File

@@ -34,10 +34,8 @@ data class OpenGroupMessage(
val quote = message.quote
if (quote != null && quote.isValid()) {
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
val quotedAttachmentID = quote.attachmentID
if (quotedAttachmentID != null) { attachmentIDs.remove(quotedAttachmentID) }
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, null)
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
} else {
null
}
@@ -51,18 +49,18 @@ data class OpenGroupMessage(
linkPreview?.let {
if (!linkPreview.isValid()) { return@let }
val attachmentID = linkPreview.attachmentID ?: return@let
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(attachmentID) ?: return@let
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
val openGroupLinkPreview = Attachment(
Attachment.Kind.LinkPreview,
server,
attachment.id,
attachment.contentType!!,
attachment.size.get(),
attachment.fileName.get(),
attachment.fileName.orNull(),
0,
attachment.width,
attachment.height,
attachment.caption.get(),
attachment.caption.orNull(),
attachment.url,
linkPreview.url,
linkPreview.title)
@@ -77,7 +75,7 @@ data class OpenGroupMessage(
attachment.id,
attachment.contentType!!,
attachment.size.orNull(),
attachment.fileName.orNull(),
attachment.fileName.orNull() ?: "",
0,
attachment.width,
attachment.height,

View File

@@ -109,7 +109,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closeGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in message.openGroups) {
@@ -220,11 +220,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!)
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
}
// Parameter @sender:String is just for inserting incoming info message
private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
// Create the group
@@ -237,7 +237,7 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
// Notify the user
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Add the group to the user's set of public keys to poll for
@@ -295,7 +295,7 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
@@ -313,7 +313,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
return
}
if (!group.members.map { it.toString() }.contains(senderPublicKey)) {
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
return
}
// Find our wrapper and decrypt it if possible
@@ -354,12 +354,13 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
val name = kind.name
storage.updateTitle(groupID, name)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
val groupPublicKey = message.groupPublicKey ?: return
@@ -368,9 +369,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
@@ -379,14 +378,25 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Send the latest encryption key pair to the added members if the current user is the admin of the group
val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!)
if (isCurrentUserAdmin) {
for (member in updateMembers) {
MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey)
if (userPublicKey == senderPublicKey) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
} else {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
}
}
}
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
@@ -435,7 +445,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
@@ -470,7 +480,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
@@ -489,7 +499,13 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey)
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
}
}
private fun isValidGroupUpdate(group: GroupRecord,

View File

@@ -21,6 +21,7 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeConfiguration
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.service.internal.push.PushTransportDetails
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
@@ -32,7 +33,6 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as S
object MessageSender {
const val groupSizeLimit = 100
// Error
sealed class Error(val description: String) : Exception() {
@@ -77,21 +77,21 @@ object MessageSender {
// Set the timestamp, sender and recipient
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
message.sender = userPublicKey
val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
deferred.reject(error)
}
try {
when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
is Destination.OpenGroup -> throw preconditionFailure
}
val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
deferred.reject(error)
}
// Validate the message
if (!message.isValid()) { throw Error.InvalidMessage }
// Stop here if this is a self-send, unless it's:
@@ -119,7 +119,7 @@ object MessageSender {
// Convert it to protobuf
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
// Serialize the protobuf
val plaintext = proto.toByteArray()
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
// Encrypt the serialized protobuf
val ciphertext: ByteArray
when (destination) {
@@ -183,15 +183,14 @@ object MessageSender {
errorCount += 1
if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed
handleFailure(it)
deferred.reject(it)
}
}
}.fail {
Log.d("Loki", "Couldn't send message due to error: $it.")
deferred.reject(it)
handleFailure(it)
}
} catch (exception: Exception) {
deferred.reject(exception)
handleFailure(exception)
}
return promise
}

View File

@@ -24,7 +24,8 @@ import org.session.libsignal.utilities.logging.Log
import java.util.*
import java.util.concurrent.ConcurrentHashMap
private val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
const val groupSizeLimit = 100
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
@@ -59,7 +60,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
@@ -99,14 +100,16 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
val admins = group.admins.map { it.serialize() }
// Send the update to the group
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Update the group
storage.updateTitle(groupID, newName)
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
}
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
@@ -135,18 +138,21 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Send closed group update messages to any new members individually
for (member in membersToAdd) {
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(member))
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
@@ -174,7 +180,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) {
@@ -183,7 +191,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
@@ -199,13 +207,14 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
val name = group.title
// Send the update to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
val sentTime = System.currentTimeMillis()
closedGroupControlMessage.sentTimestamp = sentTime
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
if (notifyUser) {
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
// Remove the group private key and unsubscribe from PNs
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
@@ -236,6 +245,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
// make sure we set the pendingKeyPair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
}
}
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
@@ -244,13 +262,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
}
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers)
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
}.always {
pendingKeyPair[groupPublicKey] = Optional.absent()
closedGroupControlMessage.sentTimestamp = sentTime
return if (force) {
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
} else {
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
null
}
}
@@ -265,7 +285,9 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
val members = group.members.map { it.serialize() }.toSet()
if (!members.contains(storage.getUserPublicKey()!!)) return
// Send the request to the group
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
}

View File

@@ -1,11 +1,53 @@
package org.session.libsession.messaging.sending_receiving
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.interfaces.Box
import com.goterl.lazycode.lazysodium.interfaces.Sign
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.utilities.KeyPairUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
object MessageSenderEncryption {
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientPublicKey: String): ByteArray{
return MessagingConfiguration.shared.sessionProtocol.encrypt(plaintext, recipientPublicKey)
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
/**
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
*
* @param plaintext the plaintext to encrypt. Must already be padded.
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
*
* @return the encrypted message.
*/
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
val context = MessagingConfiguration.shared.context
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't sign message due to error: $exception.")
throw Error.SigningFailed
}
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
try {
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
throw Error.EncryptionFailed
}
return ciphertext
}
}

View File

@@ -2,14 +2,17 @@ package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint
import nl.komponents.kovenant.functional.map
import okhttp3.*
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.logging.Log
@SuppressLint("StaticFieldLeak")
object PushNotificationAPI {
val context = MessagingConfiguration.shared.context
val server = "https://live.apns.getsession.org"

View File

@@ -17,19 +17,7 @@
package org.session.libsession.messaging.threads.recipients;
public class RecipientFormattingException extends Exception {
public RecipientFormattingException() {
super();
}
public RecipientFormattingException(String message) {
super(message);
}
public RecipientFormattingException(String message, Throwable nested) {
super(message, nested);
}
public RecipientFormattingException(Throwable nested) {
super(nested);
}
}

View File

@@ -1,76 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.messaging.threads.recipients;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class RecipientsFormatter {
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
int begin = recipient.indexOf('<');
int end = recipient.indexOf('>');
String value = recipient.substring(begin + 1, end);
if (PhoneNumberUtils.isWellFormedSmsAddress(value))
return value;
else
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
}
private static String parseRecipient(String recipient) throws RecipientFormattingException {
recipient = recipient.trim();
if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1))
return parseBracketedNumber(recipient);
if (PhoneNumberUtils.isWellFormedSmsAddress(recipient))
return recipient;
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
}
public static List<String> getRecipients(String rawText) throws RecipientFormattingException {
ArrayList<String> results = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
while (tokenizer.hasMoreTokens()) {
results.add(parseRecipient(tokenizer.nextToken()));
}
return results;
}
public static String formatNameAndNumber(String name, String number) {
// Format like this: Mike Cleron <(650) 555-1234>
// Erick Tseng <(650) 555-1212>
// Tutankhamun <tutank1341@gmail.com>
// (408) 555-1289
String formattedNumber = PhoneNumberUtils.formatNumber(number);
if (!TextUtils.isEmpty(name) && !name.equals(number)) {
return name + " <" + formattedNumber + ">";
} else {
return formattedNumber;
}
}
}

View File

@@ -1,63 +0,0 @@
package org.session.libsession.messaging.utilities
import android.content.Context
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.utilities.TextSecurePreferences.isUniversalUnidentifiedAccess
import org.session.libsession.utilities.Util.getSecretBytes
import org.session.libsignal.metadata.SignalProtos
import org.session.libsignal.service.api.crypto.UnidentifiedAccess
import org.session.libsignal.utilities.logging.Log
object UnidentifiedAccessUtil {
private val TAG = UnidentifiedAccessUtil::class.simpleName
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccess? {
val theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientPublicKey)
val ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null))
return if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
UnidentifiedAccess(theirUnidentifiedAccessKey)
} else null
}
fun getAccessForSync(context: Context): UnidentifiedAccess? {
var ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
if (isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = getSecretBytes(16)
}
return if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
UnidentifiedAccess(ourUnidentifiedAccessKey)
} else null
}
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
}
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
if (userPublicKey != null) {
return sodium.randomBytesBuf(16)
}
return null
}
private fun getUnidentifiedAccessCertificate(): ByteArray? {
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
if (userPublicKey != null) {
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
return certificate.toByteArray()
}
return null
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.utilities;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.libsignal.IdentityKey;
import org.session.libsignal.libsignal.IdentityKeyPair;
import org.session.libsignal.libsignal.InvalidKeyException;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
import org.session.libsignal.utilities.Base64;
import java.io.IOException;
/**
* Utility class for working with identity keys.
*
* @author Moxie Marlinspike
*/
public class IdentityKeyUtil {
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
@SuppressWarnings("unused")
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
public static final String LOKI_SEED = "loki_seed";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeyPair(@NonNull Context context) {
ECKeyPair keyPair = Curve.generateKeyPair();
ECPublicKey publicKey = keyPair.getPublicKey();
ECPrivateKey privateKey = keyPair.getPrivateKey();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
}
public static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
public static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
public static void delete(Context context, String key) {
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -0,0 +1,60 @@
package org.session.libsession.utilities
import android.content.Context
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.utils.Key
import com.goterl.lazycode.lazysodium.utils.KeyPair
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
object KeyPairUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun generate(): KeyPairGenerationResult {
val seed = sodium.randomBytesBuf(16)
try {
return generate(seed)
} catch (exception: Exception) {
return generate()
}
}
fun generate(seed: ByteArray): KeyPairGenerationResult {
val padding = ByteArray(16) { 0 }
val ed25519KeyPair = sodium.cryptoSignSeedKeypair(seed + padding)
val sodiumX25519KeyPair = sodium.convertKeyPairEd25519ToCurve25519(ed25519KeyPair)
val x25519KeyPair = ECKeyPair(DjbECPublicKey(sodiumX25519KeyPair.publicKey.asBytes), DjbECPrivateKey(sodiumX25519KeyPair.secretKey.asBytes))
return KeyPairGenerationResult(seed, ed25519KeyPair, x25519KeyPair)
}
fun store(context: Context, seed: ByteArray, ed25519KeyPair: KeyPair, x25519KeyPair: ECKeyPair) {
IdentityKeyUtil.save(context, IdentityKeyUtil.LOKI_SEED, Hex.toStringCondensed(seed))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(x25519KeyPair.publicKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(x25519KeyPair.privateKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
}
fun hasV2KeyPair(context: Context): Boolean {
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
}
fun getUserED25519KeyPair(context: Context): KeyPair? {
val base64EncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
val base64EncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
val ed25519PublicKey = Key.fromBytes(Base64.decode(base64EncodedED25519PublicKey))
val ed25519SecretKey = Key.fromBytes(Base64.decode(base64EncodedED25519SecretKey))
return KeyPair(ed25519PublicKey, ed25519SecretKey)
}
data class KeyPairGenerationResult(
val seed: ByteArray,
val ed25519KeyPair: KeyPair,
val x25519KeyPair: ECKeyPair
)
}

View File

@@ -7,6 +7,9 @@ import android.preference.PreferenceManager.getDefaultSharedPreferences
import android.provider.Settings
import androidx.annotation.ArrayRes
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R
import org.session.libsession.utilities.preferences.NotificationPrivacyPreference
import org.session.libsignal.utilities.logging.Log
@@ -16,6 +19,9 @@ import java.util.*
object TextSecurePreferences {
private val TAG = TextSecurePreferences::class.simpleName
private val _events = MutableSharedFlow<String>(0, 64, BufferOverflow.DROP_OLDEST)
val events get() = _events.asSharedFlow()
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
const val THEME_PREF = "pref_theme"
const val LANGUAGE_PREF = "pref_language"
@@ -101,7 +107,8 @@ object TextSecurePreferences {
// region Multi Device
private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
private const val CONFIGURATION_SYNCED = "pref_configuration_synced"
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
private const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
@@ -121,6 +128,7 @@ object TextSecurePreferences {
@JvmStatic
fun setConfigurationMessageSynced(context: Context, value: Boolean) {
setBooleanPreference(context, CONFIGURATION_SYNCED, value)
_events.tryEmit(CONFIGURATION_SYNCED)
}
@JvmStatic
@@ -321,6 +329,7 @@ object TextSecurePreferences {
@JvmStatic
fun setProfileName(context: Context, name: String?) {
setStringPreference(context, PROFILE_NAME_PREF, name)
_events.tryEmit(PROFILE_NAME_PREF)
}
@JvmStatic
@@ -746,5 +755,14 @@ object TextSecurePreferences {
fun setIsMigratingKeyPair(context: Context?, newValue: Boolean) {
setBooleanPreference(context!!, "is_migrating_key_pair", newValue)
}
@JvmStatic
fun setLastProfileUpdateTime(context: Context, profileUpdateTime: Long) {
setLongPreference(context, LAST_PROFILE_UPDATE_TIME, profileUpdateTime)
}
@JvmStatic
fun shouldUpdateProfile(context: Context, profileUpdateTime: Long) =
profileUpdateTime > getLongPreference(context, LAST_PROFILE_UPDATE_TIME, 0)
// endregion
}