add changes of latest dev

This commit is contained in:
Brice 2021-01-13 16:13:49 +11:00
parent cb5ee74a43
commit 99107d169e
33 changed files with 50848 additions and 46160 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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");

View File

@ -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());

View File

@ -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());
}
}

View File

@ -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))

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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() { }
}

View File

@ -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 ))
}
}
}

View File

@ -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))
}
}

View File

@ -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

View 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>

View File

@ -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="Weve 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 youll 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>

View File

@ -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>

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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>

View File

@ -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