mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 00:37:47 +00:00
add changes of latest dev
This commit is contained in:
parent
cb5ee74a43
commit
99107d169e
@ -38,6 +38,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
@ -562,24 +563,20 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
});
|
||||
}
|
||||
|
||||
//FIXME AC: Using this method to cleanup app data is unsafe due to potential concurrent
|
||||
// activity that still might be using the data that is being deleted here.
|
||||
// The most reliable and safe way to do this is to use official API call:
|
||||
// https://developer.android.com/reference/android/app/ActivityManager.html#clearApplicationUserData()
|
||||
// The downside is it kills the app in the process and there's no any conventional way to start
|
||||
// another activity when the task is done.
|
||||
// Dev community is in demand for such a feature, so check on it some time in the feature
|
||||
// and replace our implementation with the API call when it's safe to do so.
|
||||
// Here's a feature request related https://issuetracker.google.com/issues/174903931
|
||||
public void clearAllData() {
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
boolean wasUnlinked = TextSecurePreferences.getWasUnlinked(this);
|
||||
String displayName = TextSecurePreferences.getProfileName(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
TextSecurePreferences.setWasUnlinked(this, wasUnlinked);
|
||||
// MasterSecretUtil.clear(this);
|
||||
if (isMigratingToV2KeyPair) {
|
||||
TextSecurePreferences.setIsMigratingKeyPair(this, true);
|
||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
MasterSecretUtil.clear(this);
|
||||
if (!deleteDatabase("signal.db")) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
@ -605,38 +602,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
|
||||
@Override
|
||||
public void sendSessionRequestIfNeeded(@NotNull String publicKey) {
|
||||
// // It's never necessary to establish a session with self
|
||||
// String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
// if (publicKey.equals(userPublicKey)) { return; }
|
||||
// // Check that we don't already have a session
|
||||
// SignalProtocolAddress address = new SignalProtocolAddress(publicKey, SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
// boolean hasSession = new TextSecureSessionStore(this).containsSession(address);
|
||||
// if (hasSession) { return; }
|
||||
// // Check that we didn't already send a session request
|
||||
// LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
// boolean hasSentSessionRequest = (apiDB.getSessionRequestSentTimestamp(publicKey) != null);
|
||||
// boolean hasSentSessionRequestExpired = hasSentSessionRequestExpired(publicKey);
|
||||
// if (hasSentSessionRequestExpired) {
|
||||
// apiDB.setSessionRequestSentTimestamp(publicKey, 0);
|
||||
// }
|
||||
// if (hasSentSessionRequest && !hasSentSessionRequestExpired) { return; }
|
||||
// // Send the session request
|
||||
// long timestamp = new Date().getTime();
|
||||
// apiDB.setSessionRequestSentTimestamp(publicKey, timestamp);
|
||||
// SessionRequestMessageSendJob job = new SessionRequestMessageSendJob(publicKey, timestamp);
|
||||
// jobManager.add(job);
|
||||
|
||||
|
||||
// It's never necessary to establish a session with self
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (publicKey.equals(userPublicKey)) { return; }
|
||||
// Check that we don't already have a session
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(publicKey, SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
TextSecureSessionStore sessionStore = new TextSecureSessionStore(this);
|
||||
boolean hasSession = sessionStore.containsSession(address);
|
||||
if (!hasSession) {
|
||||
sessionStore.storeSession(address, new SessionRecord());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4,11 +4,15 @@ import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
@ -18,6 +22,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider {
|
||||
|
||||
override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? {
|
||||
@ -26,6 +31,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return databaseAttachment.toAttachmentStream(context)
|
||||
}
|
||||
|
||||
override fun getAttachmentPointer(attachmentID: String): SignalServiceAttachmentPointer? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer? {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
|
||||
@ -43,9 +52,21 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
attachmentUploadJob.onRun()
|
||||
}
|
||||
|
||||
override fun insertAttachment(messageId: Long, attachmentId: Long, stream : InputStream) {
|
||||
override fun getMessageForQuote(timestamp: Long, author: Address): Long? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getAttachmentsWithLinkPreviewFor(messageID: Long): List<Attachment> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getMessageBodyFor(messageID: Long): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insertAttachment(messageId: Long, attachmentId: Long, stream: InputStream) {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId,0), stream)
|
||||
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId, 0), stream)
|
||||
}
|
||||
|
||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||
@ -53,6 +74,16 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return smsDatabase.isOutgoingMessage(timestamp)
|
||||
}
|
||||
|
||||
override fun getMessageID(serverID: Long): Long? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun deleteMessage(messageID: Long) {
|
||||
TODO("Not yet implemented")
|
||||
//val publicChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
||||
//publicChatAPI?.deleteMessage(messageID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
|
||||
|
@ -143,6 +143,7 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabaseDelegate;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
@ -1080,7 +1081,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
builder.setTitle(getString(R.string.ConversationActivity_leave_group));
|
||||
builder.setIconAttribute(R.attr.dialog_info_icon);
|
||||
builder.setCancelable(true);
|
||||
builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group));
|
||||
|
||||
GroupRecord group = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getAddress().toGroupString()).orNull();
|
||||
List<Address> admins = group.getAdmins();
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
String message = getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group);
|
||||
for (Address admin : admins) {
|
||||
if (admin.toPhoneString().equals(userPublicKey)) {
|
||||
message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone.";
|
||||
}
|
||||
}
|
||||
|
||||
builder.setMessage(message);
|
||||
builder.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Recipient groupRecipient = getRecipient();
|
||||
String groupPublicKey;
|
||||
@ -1094,7 +1106,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
try {
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocol.leave(this, groupPublicKey);
|
||||
ClosedGroupsProtocolV2.leave(this, groupPublicKey);
|
||||
initializeEnabledCheck();
|
||||
} else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) {
|
||||
initializeEnabledCheck();
|
||||
|
@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
* The protocol format is roughly:
|
||||
*
|
||||
* 1) 16 byte random IV.
|
||||
* 2) AES-CBC(plaintext)
|
||||
* 3) HMAC-SHA1 of 1 and 2
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterCipher {
|
||||
|
||||
private static final String TAG = MasterCipher.class.getSimpleName();
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Cipher encryptingCipher;
|
||||
private final Cipher decryptingCipher;
|
||||
private final Mac hmac;
|
||||
|
||||
public MasterCipher(MasterSecret masterSecret) {
|
||||
try {
|
||||
this.masterSecret = masterSecret;
|
||||
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.hmac = Mac.getInstance("HmacSHA1");
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
public String encryptBody(@NonNull String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.session.libsignal.libsignal.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.session.libsignal.libsignal.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(@NonNull byte[] decodedBody) throws InvalidMessageException {
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
|
||||
|
||||
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
|
||||
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
|
||||
|
||||
return encrypted;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
throw new InvalidMessageException(ge);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
|
||||
byte[] encryptedBody = getEncryptedBody(cipher, body);
|
||||
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
|
||||
|
||||
return encryptedAndMacBody;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
Log.w("bodycipher", ge);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean verifyMacFor(String content, byte[] theirMac) {
|
||||
byte[] ourMac = getMacFor(content);
|
||||
Log.i(TAG, "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.i(TAG, "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w(TAG, "Macing: " + content);
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
return mac.doFinal(content.getBytes());
|
||||
} catch (GeneralSecurityException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
|
||||
try {
|
||||
byte[] decodedBody = Base64.decode(body);
|
||||
return decryptBytes(decodedBody);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Bad Base64 Encoding...", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptAndEncodeBytes(@NonNull byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(@NonNull Mac hmac, @NonNull byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
if (encryptedAndMac.length < hmac.getMacLength()) {
|
||||
throw new InvalidMessageException("length(encrypted body + MAC) < length(MAC)");
|
||||
}
|
||||
|
||||
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
|
||||
|
||||
byte[] remoteMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encrypted);
|
||||
|
||||
if (!Arrays.equals(remoteMac, localMac))
|
||||
throw new InvalidMessageException("MAC doesen't match.");
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
|
||||
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] encrypted = cipher.doFinal(body);
|
||||
byte[] iv = cipher.getIV();
|
||||
|
||||
byte[] ivAndBody = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
|
||||
|
||||
return ivAndBody;
|
||||
}
|
||||
|
||||
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
|
||||
byte[] mac = hmac.doFinal(encryptedBody);
|
||||
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
|
||||
|
||||
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
|
||||
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
|
||||
|
||||
return encryptedAndMac;
|
||||
}
|
||||
|
||||
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
|
||||
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
return decryptingCipher;
|
||||
}
|
||||
|
||||
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
return encryptingCipher;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents 1 and 2.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecret implements Parcelable {
|
||||
|
||||
private final SecretKeySpec encryptionKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
|
||||
@Override
|
||||
public MasterSecret createFromParcel(Parcel in) {
|
||||
return new MasterSecret(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MasterSecret[] newArray(int size) {
|
||||
return new MasterSecret[size];
|
||||
}
|
||||
};
|
||||
|
||||
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
private MasterSecret(Parcel in) {
|
||||
byte[] encryptionKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(encryptionKeyBytes);
|
||||
|
||||
byte[] macKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(macKeyBytes);
|
||||
|
||||
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
|
||||
public SecretKeySpec getEncryptionKey() {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(encryptionKey.getEncoded().length);
|
||||
out.writeByteArray(encryptionKey.getEncoded());
|
||||
out.writeInt(macKey.getEncoded().length);
|
||||
out.writeByteArray(macKey.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterSecret parcelClone() {
|
||||
Parcel thisParcel = Parcel.obtain();
|
||||
Parcel thatParcel = Parcel.obtain();
|
||||
byte[] bytes = null;
|
||||
|
||||
thisParcel.writeValue(this);
|
||||
bytes = thisParcel.marshall();
|
||||
|
||||
thatParcel.unmarshall(bytes, 0, bytes.length);
|
||||
thatParcel.setDataPosition(0);
|
||||
|
||||
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
|
||||
|
||||
thisParcel.recycle();
|
||||
thatParcel.recycle();
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,375 @@
|
||||
/**
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
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.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.PBEParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Helper class for generating and securely storing a MasterSecret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecretUtil {
|
||||
|
||||
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
MasterSecret masterSecret,
|
||||
String newPassphrase)
|
||||
{
|
||||
try {
|
||||
byte[] combinedSecrets = Util.combine(masterSecret.getEncryptionKey().getEncoded(),
|
||||
masterSecret.getMacKey().getEncoded());
|
||||
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(newPassphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return masterSecret;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new AssertionError(gse);
|
||||
}
|
||||
}
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
String originalPassphrase,
|
||||
String newPassphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
|
||||
changeMasterSecretPassphrase(context, masterSecret, newPassphrase);
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static MasterSecret getMasterSecret(Context context, String passphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
try {
|
||||
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
|
||||
byte[] macSalt = retrieve(context, "mac_salt");
|
||||
int iterations = retrieve(context, "passphrase_iterations", 100);
|
||||
byte[] encryptedMasterSecret = verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
|
||||
byte[] encryptionSalt = retrieve(context, "encryption_salt");
|
||||
byte[] combinedSecrets = decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
|
||||
byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
} catch (IOException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
|
||||
@Nullable MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
||||
|
||||
ECPublicKey djbPublicKey = null;
|
||||
ECPrivateKey djbPrivateKey = null;
|
||||
|
||||
if (djbPublicBytes != null) {
|
||||
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
||||
}
|
||||
|
||||
if (masterSecret != null) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
|
||||
if (djbPrivateBytes != null) {
|
||||
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
|
||||
} catch (InvalidKeyException | IOException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context,
|
||||
MasterSecret masterSecret)
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
|
||||
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
||||
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
||||
|
||||
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
|
||||
}
|
||||
|
||||
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
||||
try {
|
||||
byte[] encryptionSecret = generateEncryptionSecret();
|
||||
byte[] macSecret = generateMacSecret();
|
||||
byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(passphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasAsymmericMasterSecret(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
}
|
||||
|
||||
public static boolean isPassphraseInitialized(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return preferences.getBoolean("passphrase_initialized", false);
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, int value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putInt(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, byte[] value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putString(key, Base64.encodeBytes(value))
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, boolean value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putBoolean(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] retrieve(Context context, String key) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
String encodedValue = settings.getString(key, "");
|
||||
|
||||
if (TextUtils.isEmpty(encodedValue)) return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
}
|
||||
|
||||
private static int retrieve(Context context, String key, int defaultValue) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.getInt(key, defaultValue);
|
||||
}
|
||||
|
||||
private static byte[] generateEncryptionSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(128);
|
||||
|
||||
SecretKey key = generator.generateKey();
|
||||
return key.getEncoded();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.w("keyutil", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateMacSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
|
||||
return generator.generateKey().getEncoded();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
private static int generateIterationCount(String passphrase, byte[] salt) {
|
||||
int TARGET_ITERATION_TIME = 50; //ms
|
||||
int MINIMUM_ITERATION_COUNT = 100; //default for low-end devices
|
||||
int BENCHMARK_ITERATION_COUNT = 10000; //baseline starting iteration count
|
||||
|
||||
try {
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, BENCHMARK_ITERATION_COUNT);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
skf.generateSecret(keyspec);
|
||||
long finishTime = System.currentTimeMillis();
|
||||
|
||||
int scaledIterationTarget = (int) (((double)BENCHMARK_ITERATION_COUNT / (double)(finishTime - startTime)) * TARGET_ITERATION_TIME);
|
||||
|
||||
if (scaledIterationTarget < MINIMUM_ITERATION_COUNT) return MINIMUM_ITERATION_COUNT;
|
||||
else return scaledIterationTarget;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
return skf.generateSecret(keyspec);
|
||||
}
|
||||
|
||||
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int iterations, int opMode)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
|
||||
cipher.init(opMode, key, new PBEParameterSpec(salt, iterations));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static byte[] encryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static byte[] decryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException, IOException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static Mac getMacForPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
byte[] pbkdf2 = key.getEncoded();
|
||||
SecretKeySpec hmacKey = new SecretKeySpec(pbkdf2, "HmacSHA1");
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(hmacKey);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private static byte[] verifyMac(byte[] macSalt, int iterations, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
|
||||
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
|
||||
|
||||
byte[] givenMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encryptedData);
|
||||
|
||||
if (Arrays.equals(givenMac, localMac)) return encryptedData;
|
||||
else throw new InvalidPassphraseException("MAC Error");
|
||||
}
|
||||
|
||||
private static byte[] macWithPassphrase(byte[] macSalt, int iterations, byte[] data, String passphrase) throws GeneralSecurityException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
byte[] mac = hmac.doFinal(data);
|
||||
byte[] result = new byte[data.length + mac.length];
|
||||
|
||||
System.arraycopy(data, 0, result, 0, data.length);
|
||||
System.arraycopy(mac, 0, result, data.length, mac.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -412,7 +412,8 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -437,10 +438,12 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
private final boolean mms;
|
||||
private final String url;
|
||||
private final List<Address> admins;
|
||||
private final long createdAt;
|
||||
|
||||
public GroupRecord(String id, String title, String members, byte[] avatar,
|
||||
long avatarId, byte[] avatarKey, String avatarContentType,
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms, String url, String admins)
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms,
|
||||
String url, String admins, long createdAt)
|
||||
{
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
@ -453,6 +456,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
this.active = active;
|
||||
this.mms = mms;
|
||||
this.url = url;
|
||||
this.createdAt = createdAt;
|
||||
|
||||
if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ',');
|
||||
else this.members = new LinkedList<>();
|
||||
@ -522,5 +526,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
public String getUrl() { return url; }
|
||||
|
||||
public List<Address> getAdmins() { return admins; }
|
||||
|
||||
public long getCreatedAt() { return createdAt; }
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@ -54,12 +55,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV17 = 38;
|
||||
private static final int lokiV18_CLEAR_BG_POLL_JOBS = 39;
|
||||
//TODO Merge all "refactor" migrations to one before pushing to the main repo.
|
||||
private static final int lokiV19_REFACTOR0 = 40;
|
||||
private static final int lokiV19 = 40;
|
||||
private static final int lokiV19_REFACTOR1 = 41;
|
||||
private static final int lokiV19_REFACTOR2 = 42;
|
||||
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV19_REFACTOR1;
|
||||
private static final int DATABASE_VERSION = lokiV19;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -121,6 +123,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
|
||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
|
||||
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
||||
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
||||
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
||||
@ -217,8 +221,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
// Many classes were removed. We need to update DB structure and data to match the code changes.
|
||||
//TODO Merge "refactor" changes in one migration.
|
||||
if (oldVersion < lokiV19_REFACTOR0) {
|
||||
deleteJobRecords(db, "ServiceOutageDetectionJob");
|
||||
if (oldVersion < lokiV19) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
|
||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
|
||||
ClosedGroupsMigration.INSTANCE.perform(db);
|
||||
}
|
||||
if (oldVersion < lokiV19_REFACTOR1) {
|
||||
db.execSQL("DROP TABLE identities");
|
||||
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintOb
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||
|
||||
import java.util.Arrays;
|
||||
@ -32,6 +33,7 @@ public final class JobManagerFactories {
|
||||
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory());
|
||||
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
|
||||
|
@ -25,6 +25,7 @@ import org.session.libsignal.metadata.ProtocolLegacyMessageException;
|
||||
import org.session.libsignal.metadata.ProtocolNoSessionException;
|
||||
import org.session.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.session.libsignal.metadata.SelfSendException;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
@ -64,9 +65,11 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
@ -245,7 +248,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
||||
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
|
||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
|
||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, apiDB, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
|
||||
SignalServiceContent content = cipher.decrypt(envelope);
|
||||
|
||||
@ -268,8 +272,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
throw new UnsupportedOperationException("Device link operations are not supported!");
|
||||
} else {
|
||||
|
||||
if (message.getClosedGroupUpdate().isPresent()) {
|
||||
ClosedGroupsProtocol.handleSharedSenderKeysUpdate(context, message.getClosedGroupUpdate().get(), content.getSender());
|
||||
if (message.getClosedGroupUpdateV2().isPresent()) {
|
||||
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
|
||||
}
|
||||
|
||||
if (message.isEndSession()) {
|
||||
@ -358,6 +362,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Log.i(TAG, "Dropping UD message from self.");
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "IOException during message decryption.");
|
||||
} catch (SessionProtocol.Exception e) {
|
||||
Log.i(TAG, "Couldn't handle message due to error: " + e.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
@ -122,7 +123,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
|
@ -14,6 +14,7 @@ import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
@ -174,8 +176,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
this.members.addAll(members)
|
||||
memberListAdapter.setMembers(members)
|
||||
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
memberListAdapter.setLockedMembers(arrayListOf(userPublicKey))
|
||||
val admins = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().admins.map { it.toPhoneString() }.toMutableSet()
|
||||
admins.remove(TextSecurePreferences.getLocalNumber(this))
|
||||
memberListAdapter.setLockedMembers(admins)
|
||||
|
||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
@ -224,7 +227,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
private fun commitChanges() {
|
||||
val hasMemberListChanges = members != originalMembers
|
||||
val hasMemberListChanges = (members != originalMembers)
|
||||
|
||||
if (!hasNameChanged && !hasMemberListChanges) {
|
||||
return finish()
|
||||
@ -258,16 +261,31 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
|
||||
|
||||
if (!members.contains(userAsRecipient) && !members.map { it.address.toPhoneString() }.containsAll(originalMembers.minus(userPublicKey))) {
|
||||
val message = "Can't leave while adding or removing other members."
|
||||
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
if (isSSKBasedClosedGroup) {
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name).successUi {
|
||||
val promise: Promise<Unit, Exception>
|
||||
if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||
promise = ClosedGroupsProtocolV2.leave(this, groupPublicKey!!)
|
||||
} else {
|
||||
promise = ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
|
||||
}
|
||||
promise.successUi {
|
||||
loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
finish()
|
||||
}.failUi { exception ->
|
||||
val message = if (exception is ClosedGroupsProtocol.Error) exception.description else "An error occurred"
|
||||
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
||||
loader.fadeOut()
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
|
@ -33,10 +33,6 @@ import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.dialogs.UserDetailsBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
@ -56,6 +52,8 @@ import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionMana
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.loki.dialogs.*
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import java.io.IOException
|
||||
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
|
||||
@ -201,14 +199,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
||||
// Clear all data if this is a secondary device
|
||||
if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) {
|
||||
TextSecurePreferences.setWasUnlinked(this, true)
|
||||
ApplicationContext.getInstance(this).clearAllData()
|
||||
}
|
||||
|
||||
// Perform chat sessions reset if requested (usually happens after backup restoration).
|
||||
scheduleResetAllSessionsIfRequested(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -220,6 +210,21 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
if (hasViewedSeed || !isMasterDevice) {
|
||||
seedReminderView.visibility = View.GONE
|
||||
}
|
||||
showKeyPairMigrationSheetIfNeeded()
|
||||
showKeyPairMigrationSuccessSheetIfNeeded()
|
||||
}
|
||||
|
||||
private fun showKeyPairMigrationSheetIfNeeded() {
|
||||
if (KeyPairUtilities.hasV2KeyPair(this)) { return }
|
||||
val keyPairMigrationSheet = KeyPairMigrationBottomSheet()
|
||||
keyPairMigrationSheet.show(supportFragmentManager, keyPairMigrationSheet.tag)
|
||||
}
|
||||
|
||||
private fun showKeyPairMigrationSuccessSheetIfNeeded() {
|
||||
if (!KeyPairUtilities.hasV2KeyPair(this) || !TextSecurePreferences.getIsMigratingKeyPair(this)) { return }
|
||||
val keyPairMigrationSuccessSheet = KeyPairMigrationSuccessBottomSheet()
|
||||
keyPairMigrationSuccessSheet.show(supportFragmentManager, keyPairMigrationSuccessSheet.tag)
|
||||
TextSecurePreferences.setIsMigratingKeyPair(this, false)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@ -323,13 +328,23 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
val threadID = thread.threadId
|
||||
val recipient = thread.recipient
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(this)
|
||||
val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message
|
||||
val isClosedGroup = recipient.address.isClosedGroup
|
||||
val dialogMessage: String
|
||||
if (recipient.isGroupRecipient) {
|
||||
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
|
||||
if (group != null && group.admins.map { it.toPhoneString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
|
||||
dialogMessage = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
} else {
|
||||
dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
}
|
||||
} else {
|
||||
dialogMessage = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setMessage(dialogMessage)
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) {
|
||||
val context = this@HomeActivity as Context
|
||||
|
||||
val isClosedGroup = recipient.address.isClosedGroup
|
||||
// Send a leave group message if this is an active closed group
|
||||
if (isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||
var isSSKBasedClosedGroup: Boolean
|
||||
@ -342,7 +357,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
isSSKBasedClosedGroup = false
|
||||
}
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocol.leave(context, groupPublicKey!!)
|
||||
ClosedGroupsProtocolV2.leave(context, groupPublicKey!!)
|
||||
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
|
@ -6,11 +6,13 @@ 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.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.Hex
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
@ -45,25 +47,10 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
override fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String> {
|
||||
val ciphertext = envelope.content ?: throw SessionProtocol.Exception.NoData
|
||||
val recipientX25519PrivateKey: ByteArray
|
||||
val recipientX25519PublicKey: ByteArray
|
||||
when (envelope.type) {
|
||||
UNIDENTIFIED_SENDER_VALUE -> {
|
||||
recipientX25519PrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
recipientX25519PublicKey = Hex.fromStringCondensed(TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded())
|
||||
}
|
||||
CLOSED_GROUP_CIPHERTEXT_VALUE -> {
|
||||
val hexEncodedGroupPublicKey = envelope.source
|
||||
val sskDB = DatabaseFactory.getSSKDatabase(context)
|
||||
if (!sskDB.isSSKBasedClosedGroup(hexEncodedGroupPublicKey)) { throw SessionProtocol.Exception.InvalidGroupPublicKey }
|
||||
val hexEncodedGroupPrivateKey = sskDB.getClosedGroupPrivateKey(hexEncodedGroupPublicKey) ?: throw SessionProtocol.Exception.NoGroupPrivateKey
|
||||
recipientX25519PrivateKey = Hex.fromStringCondensed(hexEncodedGroupPrivateKey)
|
||||
recipientX25519PublicKey = Hex.fromStringCondensed(hexEncodedGroupPublicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey")
|
||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
val signatureSize = Sign.BYTES
|
||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||
|
@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.loki.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
@ -9,6 +12,10 @@ import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.*
|
||||
|
||||
@ -76,6 +83,17 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
public val openGroupProfilePictureTable = "open_group_avatar_cache"
|
||||
private val openGroupProfilePicture = "open_group_avatar"
|
||||
@JvmStatic val createOpenGroupProfilePictureTableCommand = "CREATE TABLE $openGroupProfilePictureTable ($publicChatID STRING PRIMARY KEY, $openGroupProfilePicture TEXT NULLABLE DEFAULT NULL);"
|
||||
// Closed groups (V2)
|
||||
public val closedGroupEncryptionKeyPairsTable = "closed_group_encryption_key_pairs_table"
|
||||
public val closedGroupsEncryptionKeyPairIndex = "closed_group_encryption_key_pair_index"
|
||||
public val encryptionKeyPairPublicKey = "encryption_key_pair_public_key"
|
||||
public val encryptionKeyPairPrivateKey = "encryption_key_pair_private_key"
|
||||
@JvmStatic
|
||||
val createClosedGroupEncryptionKeyPairsTable = "CREATE TABLE $closedGroupEncryptionKeyPairsTable ($closedGroupsEncryptionKeyPairIndex STRING PRIMARY KEY, $encryptionKeyPairPublicKey STRING, $encryptionKeyPairPrivateKey STRING)"
|
||||
public val closedGroupPublicKeysTable = "closed_group_public_keys_table"
|
||||
public val groupPublicKey = "group_public_key"
|
||||
@JvmStatic
|
||||
val createClosedGroupPublicKeysTable = "CREATE TABLE $closedGroupPublicKeysTable ($groupPublicKey STRING PRIMARY KEY)"
|
||||
|
||||
// region Deprecated
|
||||
private val deviceLinkCache = "loki_pairing_authorisation_cache"
|
||||
@ -387,6 +405,61 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
TextSecurePreferences.setLastSnodePoolRefreshDate(context, date)
|
||||
}
|
||||
|
||||
override fun getUserX25519KeyPair(): ECKeyPair {
|
||||
val keyPair = IdentityKeyUtil.getIdentityKeyPair(context)
|
||||
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removing05PrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
|
||||
}
|
||||
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val timestamp = Date().time.toString()
|
||||
val index = "$groupPublicKey-$timestamp"
|
||||
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
|
||||
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
|
||||
val row = wrap(mapOf( Companion.closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
|
||||
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
|
||||
database.insertOrUpdate(closedGroupEncryptionKeyPairsTable, row, "${Companion.closedGroupsEncryptionKeyPairIndex} = ?", wrap(index))
|
||||
}
|
||||
|
||||
override fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List<ECKeyPair> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val timestampsAndKeyPairs = database.getAll(closedGroupEncryptionKeyPairsTable, "${Companion.closedGroupsEncryptionKeyPairIndex} LIKE ?", wrap("$groupPublicKey%")) { cursor ->
|
||||
val timestamp = cursor.getString(cursor.getColumnIndexOrThrow(Companion.closedGroupsEncryptionKeyPairIndex)).split("-").last()
|
||||
val encryptionKeyPairPublicKey = cursor.getString(cursor.getColumnIndexOrThrow(Companion.encryptionKeyPairPublicKey))
|
||||
val encryptionKeyPairPrivateKey = cursor.getString(cursor.getColumnIndexOrThrow(Companion.encryptionKeyPairPrivateKey))
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(encryptionKeyPairPublicKey)), DjbECPrivateKey(Hex.fromStringCondensed(encryptionKeyPairPrivateKey)))
|
||||
Pair(timestamp, keyPair)
|
||||
}
|
||||
return timestampsAndKeyPairs.sortedBy { it.first.toLong() }.map { it.second }
|
||||
}
|
||||
|
||||
override fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? {
|
||||
return getClosedGroupEncryptionKeyPairs(groupPublicKey).lastOrNull()
|
||||
}
|
||||
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(closedGroupEncryptionKeyPairsTable, "${Companion.closedGroupsEncryptionKeyPairIndex} LIKE ?", wrap("$groupPublicKey%"))
|
||||
}
|
||||
|
||||
fun addClosedGroupPublicKey(groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf( Companion.groupPublicKey to groupPublicKey ))
|
||||
database.insertOrUpdate(closedGroupPublicKeysTable, row, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))
|
||||
}
|
||||
|
||||
fun getAllClosedGroupPublicKeys(): Set<String> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(closedGroupPublicKeysTable, null, null) { cursor ->
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Companion.groupPublicKey))
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun removeClosedGroupPublicKey(groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
|
||||
return setOf()
|
||||
|
@ -11,12 +11,13 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatch
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
|
||||
class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol {
|
||||
|
||||
companion object {
|
||||
// Shared
|
||||
private val closedGroupPublicKey = "closed_group_public_key"
|
||||
public val closedGroupPublicKey = "closed_group_public_key"
|
||||
// Ratchets
|
||||
private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table"
|
||||
private val currentClosedGroupRatchetTable = "closed_group_ratchet_table"
|
||||
@ -32,8 +33,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
= "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
|
||||
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
|
||||
// Private keys
|
||||
private val closedGroupPrivateKeyTable = "closed_group_private_key_table"
|
||||
private val closedGroupPrivateKey = "closed_group_private_key"
|
||||
public val closedGroupPrivateKeyTable = "closed_group_private_key_table"
|
||||
public val closedGroupPrivateKey = "closed_group_private_key"
|
||||
@JvmStatic val createClosedGroupPrivateKeyTableCommand
|
||||
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
|
||||
}
|
||||
@ -123,11 +124,14 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
|
||||
override fun getAllClosedGroupPublicKeys(): Set<String> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
|
||||
val result = mutableSetOf<String>()
|
||||
result.addAll(database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
|
||||
cursor.getString(Companion.closedGroupPublicKey)
|
||||
}.filter {
|
||||
PublicKeyValidation.isValid(it)
|
||||
}.toSet()
|
||||
})
|
||||
result.addAll(DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys())
|
||||
return result
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_key_pair_migration_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
|
||||
class KeyPairMigrationBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_key_pair_migration_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
upgradeNowButton.setOnClickListener { upgradeNow() }
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener {
|
||||
val d = dialog as BottomSheetDialog
|
||||
val bottomSheet = d.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)!!
|
||||
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
|
||||
BottomSheetBehavior.from(bottomSheet).isHideable = false
|
||||
}
|
||||
isCancelable = false
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun upgradeNow() {
|
||||
val applicationContext = requireContext().applicationContext as ApplicationContext
|
||||
applicationContext.clearAllData(true)
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_key_pair_migration_success_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
class KeyPairMigrationSuccessBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private val sessionID by lazy {
|
||||
TextSecurePreferences.getLocalNumber(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_key_pair_migration_success_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sessionIDTextView.text = sessionID
|
||||
copyButton.setOnClickListener { copySessionID() }
|
||||
okButton.setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
// Expand the bottom sheet by default
|
||||
dialog.setOnShowListener {
|
||||
val d = dialog as BottomSheetDialog
|
||||
val bottomSheet = d.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||
BottomSheetBehavior.from(bottomSheet!!).setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun copySessionID() {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", sessionID)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
|
||||
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>) : Kind() // The new encryption key pair encrypted for each member individually
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper {
|
||||
val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(20)
|
||||
.build(),
|
||||
destination,
|
||||
kind)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
builder.putByteArray("publicKey", kind.publicKey)
|
||||
builder.putString("name", kind.name)
|
||||
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.Update -> {
|
||||
builder.putString("kind", "Update")
|
||||
builder.putString("name", kind.name)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
builder.putString("kind", "EncryptionKeyPair")
|
||||
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
|
||||
builder.putString("wrappers", wrappers)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
val publicKey = data.getByteArray("publicKey")
|
||||
val name = data.getString("name")
|
||||
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
|
||||
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
"Update" -> {
|
||||
val name = data.getString("name")
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.Update(name, members)
|
||||
}
|
||||
"EncryptionKeyPair" -> {
|
||||
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
|
||||
kind = Kind.EncryptionKeyPair(wrappers)
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.ClosedGroupUpdateV2.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.Update -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupUpdateV2 = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
val serializedContentMessage = contentMessage.build().toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(destination)
|
||||
val recipient = recipient(context, destination)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl: Int
|
||||
when (kind) {
|
||||
is Kind.EncryptionKeyPair -> ttl = 4 * 24 * 60 * 60 * 1000
|
||||
else -> ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
}
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedContentMessage, false, ttl, false,
|
||||
true, false, false, false)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.ContentValues
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.utilities.getAll
|
||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.whispersystems.libsignal.ecc.DjbECPrivateKey
|
||||
import org.whispersystems.libsignal.ecc.DjbECPublicKey
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||
import java.util.*
|
||||
|
||||
object ClosedGroupsMigration {
|
||||
|
||||
fun perform(database: net.sqlcipher.database.SQLiteDatabase) {
|
||||
val publicKeys = database.getAll(SharedSenderKeysDatabase.closedGroupPrivateKeyTable, null, null) { cursor ->
|
||||
cursor.getString(SharedSenderKeysDatabase.closedGroupPublicKey)
|
||||
}.filter {
|
||||
PublicKeyValidation.isValid(it)
|
||||
}
|
||||
val keyPairs = mutableListOf<ECKeyPair>()
|
||||
for (publicKey in publicKeys) {
|
||||
val query = "${SharedSenderKeysDatabase.closedGroupPublicKey} = ?"
|
||||
val privateKey = database.get(SharedSenderKeysDatabase.closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor ->
|
||||
cursor.getString(SharedSenderKeysDatabase.closedGroupPrivateKey)
|
||||
}
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(publicKey.removing05PrefixIfNeeded())), DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
|
||||
keyPairs.add(keyPair)
|
||||
val row = ContentValues(1)
|
||||
row.put(LokiAPIDatabase.groupPublicKey, publicKey)
|
||||
database.insertOrUpdate(LokiAPIDatabase.closedGroupPublicKeysTable, row, "${LokiAPIDatabase.groupPublicKey} = ?", arrayOf( publicKey ))
|
||||
}
|
||||
for (keyPair in keyPairs) {
|
||||
// In this particular case keyPair.publicKey == groupPublicKey
|
||||
val timestamp = Date().time.toString()
|
||||
val index = "${keyPair.publicKey.serialize().toHexString()}-$timestamp"
|
||||
val encryptionKeyPairPublicKey = keyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
|
||||
val encryptionKeyPairPrivateKey = keyPair.privateKey.serialize().toHexString()
|
||||
val row = ContentValues(3)
|
||||
row.put(LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex, index)
|
||||
row.put(LokiAPIDatabase.encryptionKeyPairPublicKey, encryptionKeyPairPublicKey)
|
||||
row.put(LokiAPIDatabase.encryptionKeyPairPrivateKey, encryptionKeyPairPrivateKey)
|
||||
database.insertOrUpdate(LokiAPIDatabase.closedGroupEncryptionKeyPairsTable, row, "${LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex} = ?", arrayOf( index ))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,416 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
val groupSizeLimit = 20
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
|
||||
object InvalidUpdate : Error("Invalid group update.")
|
||||
}
|
||||
|
||||
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
Thread {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
if (member == userPublicKey) { continue }
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupID)
|
||||
}.start()
|
||||
// Return
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun leave(context: Context, groupPublicKey: String): Promise<Unit, Exception> {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return Promise.ofFail(Error.NoThread)
|
||||
}
|
||||
val name = group.title
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val newMembers: Set<String>
|
||||
val isCurrentUserAdmin = group.admins.map { it.toPhoneString() }.contains(userPublicKey)
|
||||
if (!isCurrentUserAdmin) {
|
||||
newMembers = oldMembers.minus(userPublicKey)
|
||||
} else {
|
||||
newMembers = setOf() // If the admin leaves the group is destroyed
|
||||
}
|
||||
return update(context, groupPublicKey, newMembers, name)
|
||||
}
|
||||
|
||||
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
Thread {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return@Thread deferred.reject(Error.NoThread)
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val newMembers = members.minus(oldMembers)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
return@Thread deferred.reject(Error.NoKeyPair)
|
||||
}
|
||||
val removedMembers = oldMembers.minus(members)
|
||||
if (removedMembers.contains(admins.first()) && members.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
return@Thread deferred.reject(Error.InvalidUpdate)
|
||||
}
|
||||
val isUserLeaving = removedMembers.contains(userPublicKey)
|
||||
if (isUserLeaving && members.isNotEmpty()) {
|
||||
if (removedMembers.count() != 1 || newMembers.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove self and add or remove others simultaneously.")
|
||||
return@Thread deferred.reject(Error.InvalidUpdate)
|
||||
}
|
||||
}
|
||||
// Send the update to the group
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
if (isUserLeaving) {
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
apiDB.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
} else {
|
||||
// Generate and distribute a new encryption key pair if needed
|
||||
val wasAnyUserRemoved = removedMembers.isNotEmpty()
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers))
|
||||
}
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in newMembers) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
if (!isUserLeaving) {
|
||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||
deferred.resolve(Unit)
|
||||
}.start()
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toPhoneString() }.contains(userPublicKey)) {
|
||||
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
|
||||
return
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// Distribute it
|
||||
val proto = SignalServiceProtos.ClosedGroupUpdateV2.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.mapNotNull { publicKey ->
|
||||
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
|
||||
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
|
||||
}
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers))
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(closedGroupUpdate)) { return; }
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
|
||||
else -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2): Boolean {
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
|
||||
return !closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> {
|
||||
return !closedGroupUpdate.name.isNullOrEmpty()
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> return true
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Unwrap the message
|
||||
val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString()
|
||||
val name = closedGroupUpdate.name
|
||||
val encryptionKeyPairAsProto = closedGroupUpdate.encryptionKeyPair
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
|
||||
// Create the group
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
if (groupDB.getGroup(groupID).orNull() != null) {
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
}
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
|
||||
public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Unwrap the message
|
||||
val name = closedGroupUpdate.name
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
val newMembers = members.toSet().minus(oldMembers)
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.createdAt > sentTimestamp) {
|
||||
Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
return
|
||||
}
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (!oldMembers.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return
|
||||
}
|
||||
// Check that the admin wasn't removed unless the group was destroyed entirely
|
||||
if (!members.contains(group.admins.first().toPhoneString()) && members.isNotEmpty()) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update message.")
|
||||
return
|
||||
}
|
||||
// Remove the group from the user's set of public keys to poll for if the current user was removed
|
||||
val wasCurrentUserRemoved = !members.contains(userPublicKey)
|
||||
if (wasCurrentUserRemoved) {
|
||||
apiDB.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
// Generate and distribute a new encryption key pair if needed
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toPhoneString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers))
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
if (!wasCurrentUserRemoved) {
|
||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
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
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toPhoneString() })
|
||||
}
|
||||
|
||||
private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val userKeyPair = apiDB.getUserX25519KeyPair()
|
||||
// Unwrap the message
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toPhoneString() }.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
|
||||
return
|
||||
}
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
|
||||
val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.ClosedGroupUpdateV2.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
// Store it
|
||||
apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>) {
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||
.setType(type0)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||
.setType(type)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
||||
// NOTE: Signal group ID handling is weird. The ID is double encoded in the database, but not in a `GroupContext`.
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
public fun doubleEncodeGroupID(groupPublicKey: String): String {
|
||||
return GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
public fun doubleDecodeGroupID(groupID: String): ByteArray {
|
||||
return GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID))
|
||||
}
|
||||
}
|
@ -1246,6 +1246,22 @@ public class TextSecurePreferences {
|
||||
public static void setLastSnodePoolRefreshDate(Context context, Date date) {
|
||||
setLongPreference(context, "last_snode_pool_refresh_date", date.getTime());
|
||||
}
|
||||
|
||||
public static long getLastKeyPairMigrationNudge(Context context) {
|
||||
return getLongPreference(context, "last_key_pair_migration_nudge", 0);
|
||||
}
|
||||
|
||||
public static void setLastKeyPairMigrationNudge(Context context, long newValue) {
|
||||
setLongPreference(context, "last_key_pair_migration_nudge", newValue);
|
||||
}
|
||||
|
||||
public static boolean getIsMigratingKeyPair(Context context) {
|
||||
return getBooleanPreference(context, "is_migrating_key_pair", false);
|
||||
}
|
||||
|
||||
public static void setIsMigratingKeyPair(Context context, boolean newValue) {
|
||||
setBooleanPreference(context, "is_migrating_key_pair", newValue);
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Backup related
|
||||
|
9
app/src/main/res/drawable/ic_shield.xml
Normal file
9
app/src/main/res/drawable/ic_shield.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M466.5,83.7l-192,-80a48.15,48.15 0,0 0,-36.9 0l-192,80C27.7,91.1 16,108.6 16,128c0,198.5 114.5,335.7 221.5,380.3 11.8,4.9 25.1,4.9 36.9,0C360.1,472.6 496,349.3 496,128c0,-19.4 -11.7,-36.9 -29.5,-44.3zM256.1,446.3l-0.1,-381 175.9,73.3c-3.3,151.4 -82.1,261.1 -175.8,307.7z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="@dimen/large_spacing"
|
||||
app:behavior_hideable="false"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_shield"
|
||||
android:tint="?android:textColorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:text="Session IDs Just Got Better"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/large_font_size" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:text="We’ve upgraded Session IDs to make them even more private and secure. To ensure your continued privacy you're now required to upgrade."
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:text="Your existing contacts and conversations will be lost, but you’ll be able to use Session knowing you have the best privacy and security possible."
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantOutline"
|
||||
android:id="@+id/upgradeNowButton"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:text="Upgrade Now" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="@dimen/large_spacing"
|
||||
app:behavior_hideable="true"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_shield"
|
||||
android:tint="?android:textColorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:text="Upgrade Successful!"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/large_font_size" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:text="Your new and improved Session ID is:"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/sessionIDTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18dp"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantOutline"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:text="Copy" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantOutline"
|
||||
android:id="@+id/okButton"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:text="@string/ok" />
|
||||
|
||||
</LinearLayout>
|
@ -9,6 +9,7 @@ import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.metadata.SealedSessionCipher;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.SessionBuilder;
|
||||
@ -95,6 +96,7 @@ import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLin
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster;
|
||||
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
||||
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -116,8 +118,6 @@ import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import nl.komponents.kovenant.Promise;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
/**
|
||||
* The main interface for sending Signal Service messages.
|
||||
*
|
||||
@ -1168,93 +1168,80 @@ public class SignalServiceMessageSender {
|
||||
{
|
||||
if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); }
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
try {
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, unidentifiedAccess, timestamp, content, online, useFallbackEncryption, isClosedGroup);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
// ========
|
||||
if (messages.getMessages().isEmpty()) {
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
}
|
||||
// ========
|
||||
Set<String> userLinkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey);
|
||||
if (sskDatabase.isSSKBasedClosedGroup(recipient.getNumber())) {
|
||||
Log.d("Loki", "Sending message to closed group.") ;
|
||||
} else if (recipient.getNumber().equals(userPublicKey)) {
|
||||
Log.d("Loki", "Sending message to self.");
|
||||
} else if (userLinkedDevices.contains(recipient.getNumber())) {
|
||||
Log.d("Loki", "Sending message to linked device.");
|
||||
} else {
|
||||
Log.d("Loki", "Sending message to " + recipient.getNumber() + ".");
|
||||
}
|
||||
OutgoingPushMessage message = messages.getMessages().get(0);
|
||||
final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type);
|
||||
final String senderID;
|
||||
if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) {
|
||||
senderID = recipient.getNumber();
|
||||
// } else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
|
||||
// senderID = "";
|
||||
} else {
|
||||
senderID = userPublicKey;
|
||||
}
|
||||
final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
// Make sure we have a valid ttl; otherwise default to 2 days
|
||||
if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); }
|
||||
final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular);
|
||||
final int __ttl = ttl;
|
||||
final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false);
|
||||
SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1<Set<Promise<Map<?, ?>, Exception>>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Set<Promise<Map<?, ?>, Exception>> promises) {
|
||||
final boolean[] isSuccess = { false };
|
||||
final int[] promiseCount = {promises.size()};
|
||||
final int[] errorCount = { 0 };
|
||||
for (Promise<Map<?, ?>, Exception> promise : promises) {
|
||||
promise.success(new Function1<Map<?, ?>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Map<?, ?> map) {
|
||||
if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageSent", timestamp);
|
||||
}
|
||||
isSuccess[0] = true;
|
||||
if (notifyPNServer) {
|
||||
PushNotificationAPI.shared.notify(messageInfo);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
errorCount[0] += 1;
|
||||
if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageFailed", timestamp);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
// ========
|
||||
if (messages.getMessages().isEmpty()) {
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
}
|
||||
// ========
|
||||
OutgoingPushMessage message = messages.getMessages().get(0);
|
||||
final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type);
|
||||
final String senderID;
|
||||
if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) {
|
||||
senderID = recipient.getNumber();
|
||||
} else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
|
||||
senderID = "";
|
||||
} else {
|
||||
senderID = userPublicKey;
|
||||
}
|
||||
final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
// Make sure we have a valid ttl; otherwise default to 2 days
|
||||
if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); }
|
||||
final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular);
|
||||
final int __ttl = ttl;
|
||||
final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false);
|
||||
SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1<Set<Promise<Map<?, ?>, Exception>>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Set<Promise<Map<?, ?>, Exception>> promises) {
|
||||
final boolean[] isSuccess = { false };
|
||||
final int[] promiseCount = {promises.size()};
|
||||
final int[] errorCount = { 0 };
|
||||
for (Promise<Map<?, ?>, Exception> promise : promises) {
|
||||
promise.success(new Function1<Map<?, ?>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Map<?, ?> map) {
|
||||
if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageSent", timestamp);
|
||||
}
|
||||
isSuccess[0] = true;
|
||||
if (notifyPNServer) {
|
||||
PushNotificationAPI.shared.notify(messageInfo);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
errorCount[0] += 1;
|
||||
if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageFailed", timestamp);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
try {
|
||||
f.get(1, TimeUnit.MINUTES);
|
||||
@ -1347,165 +1334,28 @@ public class SignalServiceMessageSender {
|
||||
return createAttachmentPointer(pointer);
|
||||
}
|
||||
|
||||
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] plaintext,
|
||||
boolean online,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup)
|
||||
throws IOException, InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
|
||||
// Loki - The way this works is:
|
||||
// • Alice sends a session request (i.e. a pre key bundle) to Bob using fallback encryption.
|
||||
// • She may send any number of subsequent messages also encrypted using fallback encryption.
|
||||
// • When Bob receives the session request, he sets up his Signal cipher session locally and sends back a null message,
|
||||
// now encrypted using Signal encryption.
|
||||
// • Alice receives this, sets up her Signal cipher session locally, and sends any subsequent messages
|
||||
// using Signal encryption.
|
||||
|
||||
if (!recipient.equals(localAddress) || unidentifiedAccess.isPresent()) {
|
||||
if (sskDatabase.isSSKBasedClosedGroup(recipient.getNumber()) && unidentifiedAccess.isPresent()) {
|
||||
messages.add(getSSKEncryptedMessage(recipient.getNumber(), unidentifiedAccess.get(), plaintext));
|
||||
// } else if (useFallbackEncryption) {
|
||||
// messages.add(getFallbackCipherEncryptedMessage(recipient.getNumber(), plaintext, unidentifiedAccess));
|
||||
// } else {
|
||||
// OutgoingPushMessage message = getEncryptedMessage(socket, recipient, unidentifiedAccess, plaintext, isClosedGroup);
|
||||
// if (message != null) { // May be null in a closed group context
|
||||
// messages.add(message);
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
//AC: Use unencrypted messages for private chats.
|
||||
OutgoingPushMessage message = getUnencryptedMessage(plaintext);
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, messages, online);
|
||||
}
|
||||
|
||||
private OutgoingPushMessageList getSessionProtocolEncryptedMessages(PushServiceSocket socket,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] plaintext,
|
||||
boolean online,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup)
|
||||
private OutgoingPushMessageList getSessionProtocolEncryptedMessage(SignalServiceAddress recipient, long timestamp, byte[] plaintext)
|
||||
{
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(3);
|
||||
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
|
||||
byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), publicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
boolean isSSKBasedClosedGroup = sskDatabase.isSSKBasedClosedGroup(publicKey);
|
||||
String encryptionPublicKey;
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
|
||||
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
|
||||
} else {
|
||||
encryptionPublicKey = publicKey;
|
||||
}
|
||||
byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int type = isSSKBasedClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
|
||||
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(type, 1, 0, body);
|
||||
messages.add(message);
|
||||
|
||||
return new OutgoingPushMessageList(publicKey, timestamp, messages, online);
|
||||
}
|
||||
|
||||
private OutgoingPushMessage getUnencryptedMessage(byte[] plaintext) {
|
||||
Log.d("Loki", "Bypassing cipher and preparing a plaintext message.");
|
||||
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(FallbackSessionCipher.getSessionVersion());
|
||||
byte[] bytes = transportDetails.getPaddedMessageBody(plaintext);
|
||||
return new OutgoingPushMessage(SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE, deviceID, 0, Base64.encodeBytes(bytes));
|
||||
}
|
||||
|
||||
private OutgoingPushMessage getFallbackCipherEncryptedMessage(String publicKey, byte[] plaintext, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
Log.d("Loki", "Using fallback cipher.");
|
||||
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(publicKey, deviceID);
|
||||
byte[] userPrivateKey = store.getIdentityKeyPair().getPrivateKey().serialize();
|
||||
FallbackSessionCipher cipher = new FallbackSessionCipher(userPrivateKey, publicKey);
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(FallbackSessionCipher.getSessionVersion());
|
||||
byte[] bytes = cipher.encrypt(transportDetails.getPaddedMessageBody(plaintext));
|
||||
if (bytes == null) { bytes = new byte[0]; }
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(store, null, signalProtocolAddress);
|
||||
FallbackMessage message = new FallbackMessage(bytes);
|
||||
byte[] ciphertext = sealedSessionCipher.encrypt(signalProtocolAddress, unidentifiedAccess.get().getUnidentifiedCertificate(), message);
|
||||
return new OutgoingPushMessage(SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE, deviceID, 0, Base64.encodeBytes(ciphertext));
|
||||
} else {
|
||||
return new OutgoingPushMessage(SignalServiceProtos.Envelope.Type.FALLBACK_MESSAGE_VALUE, deviceID, 0, Base64.encodeBytes(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
private OutgoingPushMessage getSSKEncryptedMessage(String groupPublicKey, UnidentifiedAccess unidentifiedAccess, byte[] plaintext)
|
||||
throws InvalidKeyException, UntrustedIdentityException, IOException
|
||||
{
|
||||
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(groupPublicKey, deviceID);
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, sessionProtocolImpl, null);
|
||||
try {
|
||||
return cipher.encrypt(signalProtocolAddress, Optional.of(unidentifiedAccess), plaintext);
|
||||
} catch (org.session.libsignal.libsignal.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted identity", groupPublicKey, e.getUntrustedIdentity());
|
||||
}
|
||||
}
|
||||
|
||||
private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
byte[] plaintext,
|
||||
boolean isClosedGroup)
|
||||
throws IOException, InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
Log.d("Loki", "Using Signal cipher.");
|
||||
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getNumber(), deviceID);
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, sessionProtocolImpl, null);
|
||||
|
||||
try {
|
||||
String contactPublicKey = recipient.getNumber();
|
||||
PreKeyBundle preKeyBundle = preKeyBundleDatabase.getPreKeyBundle(contactPublicKey);
|
||||
if (preKeyBundle == null) {
|
||||
if (!store.containsSession(signalProtocolAddress)) {
|
||||
SessionManagementProtocol.shared.repairSessionIfNeeded(recipient, isClosedGroup);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
// ========
|
||||
if (SessionManagementProtocol.shared.shouldIgnoreMissingPreKeyBundleException(isClosedGroup)) {
|
||||
return null;
|
||||
}
|
||||
// ========
|
||||
throw new InvalidKeyException("Pre key bundle not found for: " + recipient.getNumber() + ".");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(contactPublicKey, preKeyBundle.getDeviceId());
|
||||
SessionBuilder sessionBuilder = new SessionBuilder(store, address);
|
||||
sessionBuilder.process(preKeyBundle);
|
||||
// Loki - Discard the pre key bundle once the session has been established
|
||||
preKeyBundleDatabase.removePreKeyBundle(contactPublicKey);
|
||||
} catch (org.session.libsignal.libsignal.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted identity key", recipient.getNumber(), preKeyBundle.getIdentityKey());
|
||||
}
|
||||
if (eventListener.isPresent()) {
|
||||
eventListener.get().onSecurityEvent(recipient);
|
||||
}
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
// Loki - Ensure all session processing has finished
|
||||
synchronized (SESSION_LOCK) {
|
||||
try {
|
||||
return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext);
|
||||
} catch (org.session.libsignal.libsignal.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted on send", recipient.getNumber(), e.getUntrustedIdentity());
|
||||
}
|
||||
}
|
||||
return new OutgoingPushMessageList(publicKey, timestamp, messages, false);
|
||||
}
|
||||
|
||||
private Optional<UnidentifiedAccess> getTargetUnidentifiedAccess(Optional<UnidentifiedAccessPair> unidentifiedAccess) {
|
||||
|
@ -9,6 +9,7 @@ package org.session.libsignal.service.api.crypto;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.session.libsignal.metadata.InvalidMetadataVersionException;
|
||||
import org.session.libsignal.metadata.ProtocolDuplicateMessageException;
|
||||
@ -84,7 +85,9 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMes
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Verified;
|
||||
import org.session.libsignal.service.internal.util.Base64;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupUtilities;
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage;
|
||||
@ -116,6 +119,7 @@ public class SignalServiceCipher {
|
||||
private final SharedSenderKeysDatabaseProtocol sskDatabase;
|
||||
private final SignalServiceAddress localAddress;
|
||||
private final SessionProtocol sessionProtocolImpl;
|
||||
private final LokiAPIDatabaseProtocol apiDB;
|
||||
private final CertificateValidator certificateValidator;
|
||||
|
||||
public SignalServiceCipher(SignalServiceAddress localAddress,
|
||||
@ -123,6 +127,7 @@ public class SignalServiceCipher {
|
||||
SharedSenderKeysDatabaseProtocol sskDatabase,
|
||||
SessionResetProtocol sessionResetProtocol,
|
||||
SessionProtocol sessionProtocolImpl,
|
||||
LokiAPIDatabaseProtocol apiDB,
|
||||
CertificateValidator certificateValidator)
|
||||
{
|
||||
this.signalProtocolStore = signalProtocolStore;
|
||||
@ -130,6 +135,7 @@ public class SignalServiceCipher {
|
||||
this.sskDatabase = sskDatabase;
|
||||
this.localAddress = localAddress;
|
||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
||||
this.apiDB = apiDB;
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
@ -190,7 +196,7 @@ public class SignalServiceCipher {
|
||||
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
|
||||
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
|
||||
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
|
||||
SelfSendException, IOException
|
||||
SelfSendException, IOException, SessionProtocol.Exception
|
||||
|
||||
{
|
||||
try {
|
||||
@ -309,7 +315,7 @@ public class SignalServiceCipher {
|
||||
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
|
||||
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
|
||||
ProtocolInvalidKeyIdException, ProtocolNoSessionException,
|
||||
SelfSendException, IOException
|
||||
SelfSendException, IOException, SessionProtocol.Exception
|
||||
{
|
||||
try {
|
||||
SignalProtocolAddress sourceAddress = new SignalProtocolAddress(envelope.getSource(), envelope.getSourceDevice());
|
||||
@ -321,23 +327,13 @@ public class SignalServiceCipher {
|
||||
int sessionVersion;
|
||||
|
||||
if (envelope.isClosedGroupCiphertext()) {
|
||||
try {
|
||||
// Try the Session protocol
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
sessionVersion = sessionCipher.getSessionVersion();
|
||||
} catch (Exception exception) {
|
||||
// Fall back on shared sender keys
|
||||
Pair<byte[], String> plaintextAndSenderPublicKey = ClosedGroupUtilities.decrypt(envelope);
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.second();
|
||||
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob
|
||||
paddedMessage = plaintextAndSenderPublicKey.first();
|
||||
metadata = new Metadata(senderPublicKey, envelope.getSourceDevice(), envelope.getTimestamp(), false);
|
||||
sessionVersion = sessionCipher.getSessionVersion();
|
||||
}
|
||||
String groupPublicKey = envelope.getSource();
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
sessionVersion = sessionCipher.getSessionVersion();
|
||||
} else if (envelope.isPreKeySignalMessage()) {
|
||||
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
|
||||
metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
|
||||
@ -347,21 +343,12 @@ public class SignalServiceCipher {
|
||||
metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
|
||||
sessionVersion = sessionCipher.getSessionVersion();
|
||||
} else if (envelope.isUnidentifiedSender()) {
|
||||
try {
|
||||
// Try the Session protocol
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice()));
|
||||
} catch (Exception exception) {
|
||||
// Fall back on the Signal protocol
|
||||
Pair<SignalProtocolAddress, Pair<Integer, byte[]>> results = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp(), envelope.getSource());
|
||||
Pair<Integer, byte[]> data = results.second();
|
||||
paddedMessage = data.second();
|
||||
metadata = new Metadata(results.first().getName(), results.first().getDeviceId(), envelope.getTimestamp(), false);
|
||||
sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice()));
|
||||
}
|
||||
ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair();
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice()));
|
||||
} else {
|
||||
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||
}
|
||||
@ -400,6 +387,7 @@ public class SignalServiceCipher {
|
||||
List<Preview> previews = createPreviews(content);
|
||||
Sticker sticker = createSticker(content);
|
||||
ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate();
|
||||
SignalServiceProtos.ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2();
|
||||
boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0);
|
||||
|
||||
for (AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||
@ -428,6 +416,7 @@ public class SignalServiceCipher {
|
||||
null,
|
||||
null,
|
||||
closedGroupUpdate,
|
||||
closedGroupUpdateV2,
|
||||
isDeviceUnlinkingRequest);
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import org.session.libsignal.libsignal.state.PreKeyBundle;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ClosedGroupUpdateV2;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ClosedGroupUpdate;
|
||||
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
||||
@ -38,6 +39,7 @@ public class SignalServiceDataMessage {
|
||||
private final Optional<PreKeyBundle> preKeyBundle;
|
||||
private final Optional<DeviceLink> deviceLink;
|
||||
private final Optional<ClosedGroupUpdate> closedGroupUpdate;
|
||||
private final Optional<ClosedGroupUpdateV2> closedGroupUpdateV2;
|
||||
private final boolean isDeviceUnlinkingRequest;
|
||||
|
||||
/**
|
||||
@ -132,7 +134,7 @@ public class SignalServiceDataMessage {
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker)
|
||||
{
|
||||
this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, false);
|
||||
this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,7 +154,8 @@ public class SignalServiceDataMessage {
|
||||
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink,
|
||||
ClosedGroupUpdate closedGroupUpdate, boolean isDeviceUnlinkingRequest)
|
||||
ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2,
|
||||
boolean isDeviceUnlinkingRequest)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.body = Optional.fromNullable(body);
|
||||
@ -167,6 +170,7 @@ public class SignalServiceDataMessage {
|
||||
this.preKeyBundle = Optional.fromNullable(preKeyBundle);
|
||||
this.deviceLink = Optional.fromNullable(deviceLink);
|
||||
this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate);
|
||||
this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2);
|
||||
this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest;
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
@ -269,6 +273,8 @@ public class SignalServiceDataMessage {
|
||||
|
||||
public Optional<ClosedGroupUpdate> getClosedGroupUpdate() { return closedGroupUpdate; }
|
||||
|
||||
public Optional<ClosedGroupUpdateV2> getClosedGroupUpdateV2() { return closedGroupUpdateV2; }
|
||||
|
||||
public Optional<PreKeyBundle> getPreKeyBundle() { return preKeyBundle; }
|
||||
|
||||
public Optional<DeviceLink> getDeviceLink() { return deviceLink; }
|
||||
@ -405,12 +411,13 @@ public class SignalServiceDataMessage {
|
||||
|
||||
public SignalServiceDataMessage build() {
|
||||
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||
// closedGroupUpdate is always null because we don't use SignalServiceDataMessage to send them (we use ClosedGroupUpdateMessageSendJob)
|
||||
// closedGroupUpdate is always null because we don't use SignalServiceDataMessage to send them (we use ClosedGroupUpdateMessageSendJob)
|
||||
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
|
||||
expiresInSeconds, expirationUpdate, profileKey,
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
sticker, preKeyBundle, deviceLink,
|
||||
null, isDeviceUnlinkingRequest);
|
||||
null, null,
|
||||
isDeviceUnlinkingRequest);
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
package org.session.libsignal.service.loki.api.crypto
|
||||
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
|
||||
interface SessionProtocol {
|
||||
|
||||
@ -12,7 +13,7 @@ interface SessionProtocol {
|
||||
// Decryption
|
||||
object NoData : Exception("Received an empty envelope.")
|
||||
object InvalidGroupPublicKey : Exception("Invalid group public key.")
|
||||
object NoGroupPrivateKey : Exception("Missing group private key.")
|
||||
object NoGroupKeyPair : Exception("Missing group key pair.")
|
||||
object DecryptionFailed : Exception("Couldn't decrypt message.")
|
||||
object InvalidSignature : Exception("Invalid message signature.")
|
||||
}
|
||||
@ -28,13 +29,36 @@ interface SessionProtocol {
|
||||
fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray
|
||||
|
||||
/**
|
||||
* Decrypts `envelope.content` using the Session protocol. If the envelope type is `UNIDENTIFIED_SENDER` the message is assumed to be a one-to-one
|
||||
* message. If the envelope type is `CLOSED_GROUP_CIPHERTEXT` the message is assumed to be a closed group message. In the latter case `envelope.source`
|
||||
* must be set to the closed group's public key.
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
* @param envelope the envelope for which to decrypt the content.
|
||||
* @param ciphertext the data to decrypt.
|
||||
* @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
|
||||
*
|
||||
* @return the padded plaintext.
|
||||
*/
|
||||
fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String>
|
||||
fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String>
|
||||
}
|
||||
|
||||
object SessionProtocolUtilities {
|
||||
|
||||
fun decryptClosedGroupCiphertext(ciphertext: ByteArray, groupPublicKey: String, apiDB: LokiAPIDatabaseProtocol, sessionProtocolImpl: SessionProtocol): Pair<ByteArray, String> {
|
||||
val encryptionKeyPairs = apiDB.getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList()
|
||||
if (encryptionKeyPairs.isEmpty()) { throw SessionProtocol.Exception.NoGroupKeyPair }
|
||||
// Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
|
||||
// likely be the one we want) but try older ones in case that didn't work)
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
|
||||
fun decrypt(): Pair<ByteArray, String> {
|
||||
try {
|
||||
return sessionProtocolImpl.decrypt(ciphertext, encryptionKeyPair)
|
||||
} catch(exception: Exception) {
|
||||
if (encryptionKeyPairs.isNotEmpty()) {
|
||||
encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
|
||||
return decrypt()
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
return decrypt()
|
||||
}
|
||||
}
|
@ -10,22 +10,11 @@ import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||
|
||||
class LokiServiceCipher(
|
||||
localAddress: SignalServiceAddress,
|
||||
private val signalProtocolStore: SignalProtocolStore,
|
||||
private val sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||
sessionProtocolImpl: SessionProtocol,
|
||||
sessionResetProtocol: SessionResetProtocol,
|
||||
certificateValidator: CertificateValidator?)
|
||||
: SignalServiceCipher(
|
||||
localAddress,
|
||||
signalProtocolStore,
|
||||
sskDatabase,
|
||||
sessionResetProtocol,
|
||||
sessionProtocolImpl,
|
||||
certificateValidator) {
|
||||
class LokiServiceCipher(localAddress: SignalServiceAddress, private val signalProtocolStore: SignalProtocolStore, private val sskDatabase: SharedSenderKeysDatabaseProtocol, sessionProtocolImpl: SessionProtocol, sessionResetProtocol: SessionResetProtocol, apiDB: LokiAPIDatabaseProtocol, certificateValidator: CertificateValidator?) : SignalServiceCipher(localAddress, signalProtocolStore, sskDatabase, sessionResetProtocol, sessionProtocolImpl, apiDB, certificateValidator) {
|
||||
|
||||
|
||||
private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize()
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.session.libsignal.service.loki.database
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink
|
||||
import java.util.*
|
||||
@ -34,7 +35,9 @@ interface LokiAPIDatabaseProtocol {
|
||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||
fun getLastSnodePoolRefreshDate(): Date?
|
||||
fun setLastSnodePoolRefreshDate(newValue: Date)
|
||||
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
|
||||
// region Deprecated
|
||||
fun getDeviceLinks(publicKey: String): Set<DeviceLink>
|
||||
|
@ -25,7 +25,7 @@ public object TTLUtilities {
|
||||
val dayInMs = 24 * hourInMs
|
||||
return when (messageType) {
|
||||
// Unimportant control messages
|
||||
MessageType.Address, MessageType.Call, MessageType.TypingIndicator, MessageType.Verified -> 1 * minuteInMs
|
||||
MessageType.Address, MessageType.Call, MessageType.TypingIndicator, MessageType.Verified -> 20 * 1000
|
||||
// Somewhat important control messages
|
||||
MessageType.DeviceLink -> 1 * hourInMs
|
||||
// Important control messages
|
||||
|
Loading…
x
Reference in New Issue
Block a user