mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add partial support for operating as a linked device.
This commit is contained in:
committed by
Greyson Parrelli
parent
112f4bb281
commit
7203228626
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user