This commit is contained in:
Ryan ZHAO 2021-01-13 17:15:17 +11:00
commit 709727197c
34 changed files with 50843 additions and 46160 deletions

View File

@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.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.session.libsession.messaging.threads.Address;
@ -582,24 +583,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.");
}
@ -625,38 +622,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,6 +52,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentUploadJob.onRun()
}
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)
@ -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

@ -415,7 +415,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

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.session.libsession.messaging.threads.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.session.libsession.messaging.threads.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

@ -8,7 +8,7 @@ import java.util.*
class GroupRecord(
val encodedId: String, val title: String, members: String?, val avatar: ByteArray,
val avatarId: Long, val avatarKey: ByteArray, val avatarContentType: String,
val relay: String, val isActive: Boolean, val avatarDigest: ByteArray, val isMms: Boolean, val url: String, admins: String?,
val relay: String, val isActive: Boolean, val avatarDigest: ByteArray, val isMms: Boolean, val url: String, admins: String?, val createAt: Long
) {
var members: List<Address> = LinkedList<Address>()
var admins: List<Address> = LinkedList<Address>()

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,31 +1168,20 @@ 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);
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);
}
// ========
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 if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
senderID = "";
} else {
senderID = userPublicKey;
}
@ -1252,9 +1241,7 @@ public class SignalServiceMessageSender {
return Unit.INSTANCE;
}
});
} catch (InvalidKeyException e) {
throw new IOException(e);
}
@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);
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();
} 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();
}
} 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);
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()));
} 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()));
}
} 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; }
@ -410,7 +416,8 @@ public class SignalServiceDataMessage {
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