mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Add partial support for operating as a linked device.
This commit is contained in:
committed by
Greyson Parrelli
parent
112f4bb281
commit
7203228626
@@ -25,6 +25,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
@@ -32,6 +33,7 @@ import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
@@ -93,6 +95,15 @@ public class IdentityKeyUtil {
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only call when configuring as a secondary linked device.
|
||||
*/
|
||||
public static void setIdentityKeys(Context context, IdentityKeyPair identityKeyPair) {
|
||||
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Identity keys can only be set directly by a linked device");
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
|
||||
}
|
||||
|
||||
public static IdentityKeyPair generateIdentityKeyPair() {
|
||||
ECKeyPair djbKeyPair = Curve.generateKeyPair();
|
||||
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalSenderKeyStore;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
@@ -30,7 +31,7 @@ public final class SenderKeyUtil {
|
||||
* Gets when the sender key session was created, or -1 if it doesn't exist.
|
||||
*/
|
||||
public static long getCreateTimeForOurKey(@NonNull Context context, @NonNull DistributionId distributionId) {
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(Recipient.self().requireServiceId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(Recipient.self().requireServiceId(), SignalStore.account().getDeviceId());
|
||||
return SignalDatabase.senderKeys().getCreatedTime(address, distributionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@@ -12,6 +13,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
@@ -75,35 +77,7 @@ public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
||||
throw new IOException("Got a DeviceName that wasn't properly populated.");
|
||||
}
|
||||
|
||||
byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray();
|
||||
byte[] cipherText = deviceName.getCiphertext().toByteArray();
|
||||
ECPrivateKey identityKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey();
|
||||
ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0);
|
||||
byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
|
||||
byte[] cipherKey = mac.doFinal(syntheticIv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
final byte[] plaintext = cipher.doFinal(cipherText);
|
||||
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] verificationPart1 = mac.doFinal("auth".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
|
||||
byte[] verificationPart2 = mac.doFinal(plaintext);
|
||||
byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
|
||||
|
||||
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
return new Device(deviceInfo.getId(), new String(decryptName(deviceName, IdentityKeyUtil.getIdentityKeyPair(getContext()))), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e);
|
||||
@@ -114,6 +88,39 @@ public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
||||
return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static byte[] decryptName(DeviceName deviceName, IdentityKeyPair identityKeyPair) throws InvalidKeyException, GeneralSecurityException {
|
||||
byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray();
|
||||
byte[] cipherText = deviceName.getCiphertext().toByteArray();
|
||||
ECPrivateKey identityKey = identityKeyPair.getPrivateKey();
|
||||
ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0);
|
||||
byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
|
||||
byte[] cipherKey = mac.doFinal(syntheticIv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
final byte[] plaintext = cipher.doFinal(cipherText);
|
||||
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] verificationPart1 = mac.doFinal("auth".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
|
||||
byte[] verificationPart2 = mac.doFinal(plaintext);
|
||||
byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
|
||||
|
||||
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static class DeviceComparator implements Comparator<Device> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -367,5 +367,10 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
public String getPassword() {
|
||||
return SignalStore.account().getServicePassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceId() {
|
||||
return SignalStore.account().getDeviceId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.AppCapabilities;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
@@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
@@ -20,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class RefreshAttributesJob extends BaseJob {
|
||||
|
||||
@@ -94,9 +97,13 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
|
||||
boolean phoneNumberDiscoverable = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable();
|
||||
|
||||
String deviceName = SignalStore.account().getDeviceName();
|
||||
byte[] encryptedDeviceName = (deviceName == null) ? null : DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), IdentityKeyUtil.getIdentityKeyPair(context));
|
||||
|
||||
AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut());
|
||||
Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() +
|
||||
"\n Phone number discoverable : " + phoneNumberDiscoverable +
|
||||
"\n Device Name : " + (encryptedDeviceName != null) +
|
||||
"\n Capabilities:" +
|
||||
"\n Storage? " + capabilities.isStorage() +
|
||||
"\n GV2? " + capabilities.isGv2() +
|
||||
@@ -111,7 +118,8 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
registrationLockV1, registrationLockV2,
|
||||
unidentifiedAccessKey, universalUnidentifiedAccess,
|
||||
capabilities,
|
||||
phoneNumberDiscoverable);
|
||||
phoneNumberDiscoverable,
|
||||
encryptedDeviceName);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
|
||||
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
@@ -23,6 +24,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
private const val KEY_FCM_TOKEN = "account.fcm_token"
|
||||
private const val KEY_FCM_TOKEN_VERSION = "account.fcm_token_version"
|
||||
private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time"
|
||||
private const val KEY_DEVICE_NAME = "account.device_name"
|
||||
private const val KEY_DEVICE_ID = "account.device_id"
|
||||
|
||||
@VisibleForTesting
|
||||
const val KEY_E164 = "account.e164"
|
||||
@@ -131,6 +134,21 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
}
|
||||
}
|
||||
|
||||
val deviceName: String?
|
||||
get() = getString(KEY_DEVICE_NAME, null)
|
||||
|
||||
fun setDeviceName(deviceName: String) {
|
||||
putString(KEY_DEVICE_NAME, deviceName)
|
||||
}
|
||||
|
||||
var deviceId: Int by integerValue(KEY_DEVICE_ID, SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
|
||||
val isPrimaryDevice: Boolean
|
||||
get() = deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID
|
||||
|
||||
val isLinkedDevice: Boolean
|
||||
get() = !isPrimaryDevice
|
||||
|
||||
private fun clearLocalCredentials(context: Context) {
|
||||
putString(KEY_SERVICE_PASSWORD, Util.getSecret(18))
|
||||
|
||||
|
||||
@@ -316,8 +316,8 @@ public final class MessageContentProcessor {
|
||||
SignalServiceCallMessage message = content.getCallMessage().get();
|
||||
Optional<Integer> destinationDeviceId = message.getDestinationDeviceId();
|
||||
|
||||
if (destinationDeviceId.isPresent() && destinationDeviceId.get() != 1) {
|
||||
log(String.valueOf(content.getTimestamp()), String.format(Locale.US, "Ignoring call message that is not for this device! intended: %d, this: %d", destinationDeviceId.get(), 1));
|
||||
if (destinationDeviceId.isPresent() && destinationDeviceId.get() != SignalStore.account().getDeviceId()) {
|
||||
log(String.valueOf(content.getTimestamp()), String.format(Locale.US, "Ignoring call message that is not for this device! intended: %d, this: %d", destinationDeviceId.get(), SignalStore.account().getDeviceId()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1852,6 +1852,11 @@ public final class MessageContentProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (decryptionErrorMessage.getDeviceId() != SignalStore.account().getDeviceId()) {
|
||||
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] Received a DecryptionErrorMessage targeting a linked device. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
long sentTimestamp = decryptionErrorMessage.getTimestamp();
|
||||
|
||||
warn(content.getTimestamp(), "[RetryReceipt] Received a retry receipt from " + formatSender(senderRecipient, content) + " for message with timestamp " + sentTimestamp + ".");
|
||||
@@ -1932,8 +1937,7 @@ public final class MessageContentProcessor {
|
||||
private void handleIndividualRetryReceipt(@NonNull Recipient requester, @Nullable MessageLogEntry messageLogEntry, @NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) {
|
||||
boolean archivedSession = false;
|
||||
|
||||
if (decryptionErrorMessage.getDeviceId() == SignalServiceAddress.DEFAULT_DEVICE_ID &&
|
||||
decryptionErrorMessage.getRatchetKey().isPresent() &&
|
||||
if (decryptionErrorMessage.getRatchetKey().isPresent() &&
|
||||
SessionUtil.ratchetKeyMatches(requester, content.getSenderDevice(), decryptionErrorMessage.getRatchetKey().get()))
|
||||
{
|
||||
warn(content.getTimestamp(), "[RetryReceipt-I] Ratchet key matches. Archiving the session.");
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendRetryReceiptJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState;
|
||||
@@ -42,7 +43,6 @@ import org.thoughtcrime.securesms.notifications.NotificationIds;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
@@ -79,7 +79,7 @@ public final class MessageDecryptionUtil {
|
||||
public static @NonNull DecryptionResult decrypt(@NonNull Context context, @NonNull SignalServiceEnvelope envelope) {
|
||||
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(Recipient.self().requireAci(), Recipient.self().requireE164());
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore, ReentrantSessionLock.INSTANCE, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, SignalStore.account().getDeviceId(), axolotlStore, ReentrantSessionLock.INSTANCE, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
List<Job> jobs = new LinkedList<>();
|
||||
|
||||
if (envelope.isPreKeySignalMessage()) {
|
||||
|
||||
@@ -21,6 +21,7 @@ public class AccountManagerFactory {
|
||||
public static @NonNull SignalServiceAccountManager createAuthenticated(@NonNull Context context,
|
||||
@NonNull ACI aci,
|
||||
@NonNull String number,
|
||||
int deviceId,
|
||||
@NonNull String password)
|
||||
{
|
||||
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored(number)) {
|
||||
@@ -36,6 +37,7 @@ public class AccountManagerFactory {
|
||||
return new SignalServiceAccountManager(ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
|
||||
aci,
|
||||
number,
|
||||
deviceId,
|
||||
password,
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
@@ -46,6 +48,7 @@ public class AccountManagerFactory {
|
||||
*/
|
||||
public static @NonNull SignalServiceAccountManager createUnauthenticated(@NonNull Context context,
|
||||
@NonNull String number,
|
||||
int deviceId,
|
||||
@NonNull String password)
|
||||
{
|
||||
if (new SignalServiceNetworkAccess(context).isCensored(number)) {
|
||||
@@ -59,7 +62,7 @@ public class AccountManagerFactory {
|
||||
}
|
||||
|
||||
return new SignalServiceAccountManager(new SignalServiceNetworkAccess(context).getConfiguration(number),
|
||||
null, number, password, BuildConfig.SIGNAL_AGENT, FeatureFlags.okHttpAutomaticRetry());
|
||||
null, number, deviceId, password, BuildConfig.SIGNAL_AGENT, FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
@@ -138,7 +138,7 @@ public final class RegistrationRepository {
|
||||
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
|
||||
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), registrationData.getPassword());
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword());
|
||||
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
|
||||
|
||||
if (registrationData.isFcm()) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
@@ -39,7 +40,7 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
|
||||
return Single.fromCallable {
|
||||
val fcmToken: Optional<String> = FcmUtil.getToken()
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, password)
|
||||
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164, PUSH_REQUEST_TIMEOUT)
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
@@ -57,6 +58,7 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
|
||||
context,
|
||||
registrationData.e164,
|
||||
SignalServiceAddress.DEFAULT_DEVICE_ID,
|
||||
registrationData.password
|
||||
)
|
||||
|
||||
@@ -80,6 +82,7 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
|
||||
context,
|
||||
registrationData.e164,
|
||||
SignalServiceAddress.DEFAULT_DEVICE_ID,
|
||||
registrationData.password
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.registration.secondary
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.devicelist.DeviceNameProtos
|
||||
import org.whispersystems.libsignal.IdentityKeyPair
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||
import java.nio.charset.Charset
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Use to encrypt a secondary/linked device name.
|
||||
*/
|
||||
object DeviceNameCipher {
|
||||
|
||||
private const val SYNTHETIC_IV_LENGTH = 16
|
||||
|
||||
@JvmStatic
|
||||
fun encryptDeviceName(plaintext: ByteArray, identityKeyPair: IdentityKeyPair): ByteArray {
|
||||
val ephemeralKeyPair: ECKeyPair = Curve.generateKeyPair()
|
||||
val masterSecret: ByteArray = Curve.calculateAgreement(identityKeyPair.publicKey.publicKey, ephemeralKeyPair.privateKey)
|
||||
|
||||
val syntheticIv: ByteArray = computeSyntheticIv(masterSecret, plaintext)
|
||||
val cipherKey: ByteArray = computeCipherKey(masterSecret, syntheticIv)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(ByteArray(16)))
|
||||
val cipherText = cipher.doFinal(plaintext)
|
||||
|
||||
return DeviceNameProtos.DeviceName.newBuilder()
|
||||
.setEphemeralPublic(ByteString.copyFrom(ephemeralKeyPair.publicKey.serialize()))
|
||||
.setSyntheticIv(ByteString.copyFrom(syntheticIv))
|
||||
.setCiphertext(ByteString.copyFrom(cipherText))
|
||||
.build()
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
private fun computeCipherKey(masterSecret: ByteArray, syntheticIv: ByteArray): ByteArray {
|
||||
val input = "cipher".toByteArray(Charset.forName("UTF-8"))
|
||||
|
||||
val keyMac = Mac.getInstance("HmacSHA256")
|
||||
keyMac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val cipherKeyKey: ByteArray = keyMac.doFinal(input)
|
||||
|
||||
val cipherMac = Mac.getInstance("HmacSHA256")
|
||||
cipherMac.init(SecretKeySpec(cipherKeyKey, "HmacSHA256"))
|
||||
return cipherMac.doFinal(syntheticIv)
|
||||
}
|
||||
|
||||
private fun computeSyntheticIv(masterSecret: ByteArray, plaintext: ByteArray): ByteArray {
|
||||
val input = "auth".toByteArray(Charset.forName("UTF-8"))
|
||||
|
||||
val keyMac = Mac.getInstance("HmacSHA256")
|
||||
keyMac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val syntheticIvKey: ByteArray = keyMac.doFinal(input)
|
||||
|
||||
val ivMac = Mac.getInstance("HmacSHA256")
|
||||
ivMac.init(SecretKeySpec(syntheticIvKey, "HmacSHA256"))
|
||||
return ivMac.doFinal(plaintext).sliceArray(0 until SYNTHETIC_IV_LENGTH)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.thoughtcrime.securesms.registration.secondary
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
import org.whispersystems.libsignal.IdentityKeyPair
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey
|
||||
import org.whispersystems.libsignal.kdf.HKDF
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningProtos
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.UUID
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Used to decrypt a secondary/link device provisioning message from the primary device.
|
||||
*/
|
||||
class SecondaryProvisioningCipher private constructor(private val secondaryIdentityKeyPair: IdentityKeyPair) {
|
||||
|
||||
val secondaryDevicePublicKey: IdentityKey = secondaryIdentityKeyPair.publicKey
|
||||
|
||||
fun decrypt(envelope: ProvisioningProtos.ProvisionEnvelope): ProvisionDecryptResult {
|
||||
val primaryEphemeralPublicKey = envelope.publicKey.toByteArray()
|
||||
val body = envelope.body.toByteArray()
|
||||
|
||||
val provisionMessageLength = body.size - VERSION_LENGTH - IV_LENGTH - MAC_LENGTH
|
||||
|
||||
if (provisionMessageLength <= 0) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val version = body[0].toInt()
|
||||
if (version != 1) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val iv = body.sliceArray(1 until (1 + IV_LENGTH))
|
||||
val theirMac = body.sliceArray(body.size - MAC_LENGTH until body.size)
|
||||
val message = body.sliceArray(0 until body.size - MAC_LENGTH)
|
||||
val cipherText = body.sliceArray((1 + IV_LENGTH) until body.size - MAC_LENGTH)
|
||||
|
||||
val sharedSecret = Curve.calculateAgreement(ECPublicKey(primaryEphemeralPublicKey), secondaryIdentityKeyPair.privateKey)
|
||||
val derivedSecret: ByteArray = HKDF.deriveSecrets(sharedSecret, PrimaryProvisioningCipher.PROVISIONING_MESSAGE.toByteArray(), 64)
|
||||
|
||||
val cipherKey = derivedSecret.sliceArray(0 until 32)
|
||||
val macKey = derivedSecret.sliceArray(32 until 64)
|
||||
|
||||
val ourHmac = getMac(macKey, message)
|
||||
|
||||
if (!MessageDigest.isEqual(theirMac, ourHmac)) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val plaintext = try {
|
||||
getPlaintext(cipherKey, iv, cipherText)
|
||||
} catch (e: Exception) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val provisioningMessage = ProvisioningProtos.ProvisionMessage.parseFrom(plaintext)
|
||||
|
||||
return ProvisionDecryptResult.Success(
|
||||
uuid = UuidUtil.parseOrThrow(provisioningMessage.uuid),
|
||||
e164 = provisioningMessage.number,
|
||||
identityKeyPair = IdentityKeyPair(IdentityKey(provisioningMessage.identityKeyPublic.toByteArray()), Curve.decodePrivatePoint(provisioningMessage.identityKeyPrivate.toByteArray())),
|
||||
profileKey = ProfileKey(provisioningMessage.profileKey.toByteArray()),
|
||||
areReadReceiptsEnabled = provisioningMessage.readReceipts,
|
||||
primaryUserAgent = provisioningMessage.userAgent,
|
||||
provisioningCode = provisioningMessage.provisioningCode,
|
||||
provisioningVersion = provisioningMessage.provisioningVersion
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMac(key: ByteArray, message: ByteArray): ByteArray? {
|
||||
return try {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(key, "HmacSHA256"))
|
||||
mac.doFinal(message)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw AssertionError(e)
|
||||
} catch (e: InvalidKeyException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPlaintext(key: ByteArray, iv: ByteArray, message: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
return cipher.doFinal(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VERSION_LENGTH = 1
|
||||
private const val IV_LENGTH = 16
|
||||
private const val MAC_LENGTH = 32
|
||||
|
||||
fun generate(): SecondaryProvisioningCipher {
|
||||
return SecondaryProvisioningCipher(IdentityKeyUtil.generateIdentityKeyPair())
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ProvisionDecryptResult {
|
||||
object Error : ProvisionDecryptResult()
|
||||
|
||||
data class Success(
|
||||
val uuid: UUID,
|
||||
val e164: String,
|
||||
val identityKeyPair: IdentityKeyPair,
|
||||
val profileKey: ProfileKey,
|
||||
val areReadReceiptsEnabled: Boolean,
|
||||
val primaryUserAgent: String?,
|
||||
val provisioningCode: String,
|
||||
val provisioningVersion: Int
|
||||
) : ProvisionDecryptResult()
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
@@ -59,7 +60,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
CallManager.CallMediaType callMediaType = WebRtcUtil.getCallMediaTypeFromOfferType(offerType);
|
||||
|
||||
try {
|
||||
webRtcInteractor.getCallManager().call(remotePeer, callMediaType, 1);
|
||||
webRtcInteractor.getCallManager().call(remotePeer, callMediaType, SignalStore.account().getDeviceId());
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "Unable to create outgoing call: ", e);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -222,8 +223,8 @@ public abstract class WebRtcActionProcessor {
|
||||
offerMetadata.getOpaque(),
|
||||
messageAgeSec,
|
||||
WebRtcUtil.getCallMediaTypeFromOfferType(offerMetadata.getOfferType()),
|
||||
1,
|
||||
true,
|
||||
SignalStore.account().getDeviceId(),
|
||||
SignalStore.account().isPrimaryDevice(),
|
||||
remoteIdentityKey,
|
||||
localIdentityKey);
|
||||
} catch (CallException | InvalidKeyException e) {
|
||||
@@ -685,7 +686,7 @@ public abstract class WebRtcActionProcessor {
|
||||
try {
|
||||
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
|
||||
opaqueMessageMetadata.getRemoteDeviceId(),
|
||||
1,
|
||||
SignalStore.account().getDeviceId(),
|
||||
opaqueMessageMetadata.getOpaque(),
|
||||
opaqueMessageMetadata.getMessageAgeSeconds());
|
||||
} catch (CallException e) {
|
||||
|
||||
@@ -158,26 +158,6 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.serverGuid = fragments.get(0).getServerGuid();
|
||||
}
|
||||
|
||||
protected IncomingTextMessage(@NonNull RecipientId sender, @Nullable GroupId groupId)
|
||||
{
|
||||
this.message = "";
|
||||
this.sender = sender;
|
||||
this.senderDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
this.protocol = 31338;
|
||||
this.serviceCenterAddress = "Outgoing";
|
||||
this.replyPathPresent = true;
|
||||
this.pseudoSubject = "";
|
||||
this.sentTimestampMillis = System.currentTimeMillis();
|
||||
this.serverTimestampMillis = sentTimestampMillis;
|
||||
this.receivedTimestampMillis = sentTimestampMillis;
|
||||
this.groupId = groupId;
|
||||
this.push = true;
|
||||
this.subscriptionId = -1;
|
||||
this.expiresInMillis = 0;
|
||||
this.unidentified = false;
|
||||
this.serverGuid = null;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -144,7 +145,7 @@ public final class IdentityUtil {
|
||||
public static void saveIdentity(String user, IdentityKey identityKey) {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
SessionStore sessionStore = ApplicationDependencies.getSessionStore();
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(user, 1);
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(user, SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
|
||||
if (ApplicationDependencies.getIdentityStore().saveIdentity(address, identityKey)) {
|
||||
if (sessionStore.containsSession(address)) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
|
||||
@@ -152,7 +153,7 @@ public final class SignalProxyUtil {
|
||||
private static boolean testWebsocketConnectionUnregistered(long timeout) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicBoolean success = new AtomicBoolean(false);
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(ApplicationDependencies.getApplication(), "", "");
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(ApplicationDependencies.getApplication(), "", SignalServiceAddress.DEFAULT_DEVICE_ID, "");
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user