Add partial support for operating as a linked device.

This commit is contained in:
Cody Henthorne
2022-01-11 14:53:21 -05:00
committed by Greyson Parrelli
parent 112f4bb281
commit 7203228626
33 changed files with 569 additions and 109 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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