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

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