mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Improve the storage controller for regV5.
This commit is contained in:
committed by
Michelle Tang
parent
6877b9163b
commit
d2c8b6e14c
@@ -10,191 +10,159 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.registration.KeyMaterial
|
||||
import org.signal.registration.NetworkController
|
||||
import org.signal.registration.NewRegistrationData
|
||||
import org.signal.registration.PreExistingRegistrationData
|
||||
import org.signal.registration.StorageController
|
||||
import org.signal.registration.proto.ProvisioningData
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.sample.storage.RegistrationDatabase
|
||||
import org.signal.registration.sample.storage.RegistrationPreferences
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Implementation of [StorageController] that persists registration data using
|
||||
* SharedPreferences for simple key-value data and SQLite for prekeys.
|
||||
*/
|
||||
class DemoStorageController(context: Context) : StorageController {
|
||||
class DemoStorageController(private val context: Context) : StorageController {
|
||||
|
||||
companion object {
|
||||
private const val MAX_SVR_CREDENTIALS = 10
|
||||
private const val TEMP_PROTO_FILENAME = "registration_data.pb"
|
||||
}
|
||||
|
||||
private val db = RegistrationDatabase(context)
|
||||
|
||||
override suspend fun generateAndStoreKeyMaterial(
|
||||
existingAccountEntropyPool: AccountEntropyPool?,
|
||||
existingAciIdentityKeyPair: IdentityKeyPair?,
|
||||
existingPniIdentityKeyPair: IdentityKeyPair?
|
||||
): KeyMaterial = withContext(Dispatchers.IO) {
|
||||
val accountEntropyPool = existingAccountEntropyPool ?: AccountEntropyPool.generate()
|
||||
val aciIdentityKeyPair = existingAciIdentityKeyPair ?: IdentityKeyPair.generate()
|
||||
val pniIdentityKeyPair = existingPniIdentityKeyPair ?: IdentityKeyPair.generate()
|
||||
|
||||
val aciSignedPreKeyId = generatePreKeyId()
|
||||
val pniSignedPreKeyId = generatePreKeyId()
|
||||
val aciKyberPreKeyId = generatePreKeyId()
|
||||
val pniKyberPreKeyId = generatePreKeyId()
|
||||
|
||||
val timestamp = System.currentTimeMillis()
|
||||
|
||||
val aciSignedPreKey = generateSignedPreKey(aciSignedPreKeyId, timestamp, aciIdentityKeyPair)
|
||||
val pniSignedPreKey = generateSignedPreKey(pniSignedPreKeyId, timestamp, pniIdentityKeyPair)
|
||||
val aciLastResortKyberPreKey = generateKyberPreKey(aciKyberPreKeyId, timestamp, aciIdentityKeyPair)
|
||||
val pniLastResortKyberPreKey = generateKyberPreKey(pniKyberPreKeyId, timestamp, pniIdentityKeyPair)
|
||||
|
||||
val aciRegistrationId = generateRegistrationId()
|
||||
val pniRegistrationId = generateRegistrationId()
|
||||
val profileKey = generateProfileKey()
|
||||
val unidentifiedAccessKey = deriveUnidentifiedAccessKey(profileKey)
|
||||
val password = generatePassword()
|
||||
|
||||
val keyMaterial = KeyMaterial(
|
||||
aciIdentityKeyPair = aciIdentityKeyPair,
|
||||
aciSignedPreKey = aciSignedPreKey,
|
||||
aciLastResortKyberPreKey = aciLastResortKyberPreKey,
|
||||
pniIdentityKeyPair = pniIdentityKeyPair,
|
||||
pniSignedPreKey = pniSignedPreKey,
|
||||
pniLastResortKyberPreKey = pniLastResortKyberPreKey,
|
||||
aciRegistrationId = aciRegistrationId,
|
||||
pniRegistrationId = pniRegistrationId,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
servicePassword = password,
|
||||
accountEntropyPool = accountEntropyPool
|
||||
)
|
||||
|
||||
storeKeyMaterial(keyMaterial, profileKey)
|
||||
|
||||
keyMaterial
|
||||
}
|
||||
|
||||
override suspend fun saveNewRegistrationData(newRegistrationData: NewRegistrationData) = withContext(Dispatchers.IO) {
|
||||
RegistrationPreferences.saveRegistrationData(newRegistrationData)
|
||||
}
|
||||
|
||||
override suspend fun getPreExistingRegistrationData(): PreExistingRegistrationData? = withContext(Dispatchers.IO) {
|
||||
RegistrationPreferences.getPreExistingRegistrationData()
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(Dispatchers.IO) {
|
||||
File(context.filesDir, TEMP_PROTO_FILENAME).takeIf { it.exists() }?.delete()
|
||||
RegistrationPreferences.clearAll()
|
||||
RegistrationPreferences.clearRestoredSvr2Credentials()
|
||||
db.clearAllPreKeys()
|
||||
}
|
||||
|
||||
override suspend fun saveValidatedPinAndTemporaryMasterKey(pin: String, isAlphanumeric: Boolean, masterKey: MasterKey, registrationLockEnabled: Boolean) = withContext(Dispatchers.IO) {
|
||||
RegistrationPreferences.pin = pin
|
||||
RegistrationPreferences.pinAlphanumeric = isAlphanumeric
|
||||
RegistrationPreferences.temporaryMasterKey = masterKey
|
||||
RegistrationPreferences.registrationLockEnabled = registrationLockEnabled
|
||||
override suspend fun readInProgressRegistrationData(): RegistrationData = withContext(Dispatchers.IO) {
|
||||
val file = File(context.filesDir, TEMP_PROTO_FILENAME)
|
||||
if (file.exists()) {
|
||||
RegistrationData.ADAPTER.decode(file.readBytes())
|
||||
} else {
|
||||
RegistrationData()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRestoredSvrCredentials(): List<NetworkController.SvrCredentials> = withContext(Dispatchers.IO) {
|
||||
RegistrationPreferences.restoredSvr2Credentials
|
||||
override suspend fun updateInProgressRegistrationData(updater: RegistrationData.Builder.() -> Unit) = withContext(Dispatchers.IO) {
|
||||
val current = readInProgressRegistrationData()
|
||||
val updated = current.newBuilder().apply(updater).build()
|
||||
writeRegistrationData(updated)
|
||||
}
|
||||
|
||||
override suspend fun appendSvrCredentials(credentials: List<NetworkController.SvrCredentials>) = withContext(Dispatchers.IO) {
|
||||
val existing = RegistrationPreferences.restoredSvr2Credentials
|
||||
val combined = (existing + credentials).distinctBy { it.username }.takeLast(MAX_SVR_CREDENTIALS)
|
||||
RegistrationPreferences.restoredSvr2Credentials = combined
|
||||
override suspend fun commitRegistrationData() = withContext(Dispatchers.IO) {
|
||||
val file = File(context.filesDir, TEMP_PROTO_FILENAME)
|
||||
val data = RegistrationData.ADAPTER.decode(file.readBytes())
|
||||
|
||||
// Key material
|
||||
if (data.aciIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.aciIdentityKeyPair = IdentityKeyPair(data.aciIdentityKeyPair.toByteArray())
|
||||
}
|
||||
if (data.pniIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.pniIdentityKeyPair = IdentityKeyPair(data.pniIdentityKeyPair.toByteArray())
|
||||
}
|
||||
if (data.aciRegistrationId != 0) {
|
||||
RegistrationPreferences.aciRegistrationId = data.aciRegistrationId
|
||||
}
|
||||
if (data.pniRegistrationId != 0) {
|
||||
RegistrationPreferences.pniRegistrationId = data.pniRegistrationId
|
||||
}
|
||||
if (data.servicePassword.isNotEmpty()) {
|
||||
RegistrationPreferences.servicePassword = data.servicePassword
|
||||
}
|
||||
if (data.accountEntropyPool.isNotEmpty()) {
|
||||
RegistrationPreferences.aep = AccountEntropyPool(data.accountEntropyPool)
|
||||
}
|
||||
|
||||
// Pre-keys
|
||||
if (data.aciSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, SignedPreKeyRecord(data.aciSignedPreKey.toByteArray()))
|
||||
}
|
||||
if (data.pniSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, SignedPreKeyRecord(data.pniSignedPreKey.toByteArray()))
|
||||
}
|
||||
if (data.aciLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, KyberPreKeyRecord(data.aciLastResortKyberPreKey.toByteArray()))
|
||||
}
|
||||
if (data.pniLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, KyberPreKeyRecord(data.pniLastResortKyberPreKey.toByteArray()))
|
||||
}
|
||||
|
||||
// Account identity
|
||||
if (data.e164.isNotEmpty() && data.aci.isNotEmpty() && data.pni.isNotEmpty() && data.servicePassword.isNotEmpty() && data.accountEntropyPool.isNotEmpty()) {
|
||||
RegistrationPreferences.saveRegistrationData(
|
||||
NewRegistrationData(
|
||||
e164 = data.e164,
|
||||
aci = ACI.parseOrThrow(data.aci),
|
||||
pni = PNI.parseOrThrow(data.pni),
|
||||
servicePassword = data.servicePassword,
|
||||
aep = AccountEntropyPool(data.accountEntropyPool)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// PIN data
|
||||
if (data.pin.isNotEmpty()) {
|
||||
RegistrationPreferences.pin = data.pin
|
||||
RegistrationPreferences.pinAlphanumeric = data.pinIsAlphanumeric
|
||||
}
|
||||
if (data.temporaryMasterKey.size > 0) {
|
||||
RegistrationPreferences.temporaryMasterKey = MasterKey(data.temporaryMasterKey.toByteArray())
|
||||
}
|
||||
RegistrationPreferences.registrationLockEnabled = data.registrationLockEnabled
|
||||
|
||||
// SVR credentials
|
||||
if (data.svrCredentials.isNotEmpty()) {
|
||||
RegistrationPreferences.restoredSvr2Credentials = data.svrCredentials.map {
|
||||
NetworkController.SvrCredentials(username = it.username, password = it.password)
|
||||
}
|
||||
}
|
||||
|
||||
// Provisioning data
|
||||
data.provisioningData?.let { prov ->
|
||||
RegistrationPreferences.saveProvisioningData(
|
||||
NetworkController.ProvisioningMessage(
|
||||
accountEntropyPool = data.accountEntropyPool,
|
||||
e164 = data.e164,
|
||||
pin = data.pin.ifEmpty { null },
|
||||
aciIdentityKeyPair = IdentityKeyPair(data.aciIdentityKeyPair.toByteArray()),
|
||||
pniIdentityKeyPair = IdentityKeyPair(data.pniIdentityKeyPair.toByteArray()),
|
||||
platform = when (prov.platform) {
|
||||
ProvisioningData.Platform.ANDROID -> NetworkController.ProvisioningMessage.Platform.ANDROID
|
||||
ProvisioningData.Platform.IOS -> NetworkController.ProvisioningMessage.Platform.IOS
|
||||
else -> NetworkController.ProvisioningMessage.Platform.ANDROID
|
||||
},
|
||||
tier = when (prov.tier) {
|
||||
ProvisioningData.Tier.FREE -> NetworkController.ProvisioningMessage.Tier.FREE
|
||||
ProvisioningData.Tier.PAID -> NetworkController.ProvisioningMessage.Tier.PAID
|
||||
else -> null
|
||||
},
|
||||
backupTimestampMs = prov.backupTimestampMs,
|
||||
backupSizeBytes = prov.backupSizeBytes,
|
||||
restoreMethodToken = prov.restoreMethodToken,
|
||||
backupVersion = prov.backupVersion
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
override suspend fun saveNewlyCreatedPin(pin: String, isAlphanumeric: Boolean) {
|
||||
RegistrationPreferences.pin = pin
|
||||
RegistrationPreferences.pinAlphanumeric = isAlphanumeric
|
||||
}
|
||||
|
||||
override suspend fun saveProvisioningData(provisioningMessage: NetworkController.ProvisioningMessage) = withContext(Dispatchers.IO) {
|
||||
RegistrationPreferences.saveProvisioningData(provisioningMessage)
|
||||
}
|
||||
|
||||
private fun storeKeyMaterial(keyMaterial: KeyMaterial, profileKey: ProfileKey) {
|
||||
// Clear existing data
|
||||
RegistrationPreferences.clearKeyMaterial()
|
||||
db.clearAllPreKeys()
|
||||
|
||||
// Store in SharedPreferences
|
||||
RegistrationPreferences.aciIdentityKeyPair = keyMaterial.aciIdentityKeyPair
|
||||
RegistrationPreferences.pniIdentityKeyPair = keyMaterial.pniIdentityKeyPair
|
||||
RegistrationPreferences.aciRegistrationId = keyMaterial.aciRegistrationId
|
||||
RegistrationPreferences.pniRegistrationId = keyMaterial.pniRegistrationId
|
||||
RegistrationPreferences.profileKey = profileKey
|
||||
|
||||
// Store prekeys in database
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, keyMaterial.aciSignedPreKey)
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, keyMaterial.pniSignedPreKey)
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, keyMaterial.aciLastResortKyberPreKey)
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, keyMaterial.pniLastResortKyberPreKey)
|
||||
}
|
||||
|
||||
private fun generateSignedPreKey(id: Int, timestamp: Long, identityKeyPair: IdentityKeyPair): SignedPreKeyRecord {
|
||||
val keyPair = ECKeyPair.generate()
|
||||
val signature = identityKeyPair.privateKey.calculateSignature(keyPair.publicKey.serialize())
|
||||
return SignedPreKeyRecord(id, timestamp, keyPair, signature)
|
||||
}
|
||||
|
||||
private fun generateKyberPreKey(id: Int, timestamp: Long, identityKeyPair: IdentityKeyPair): KyberPreKeyRecord {
|
||||
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
|
||||
val signature = identityKeyPair.privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
|
||||
return KyberPreKeyRecord(id, timestamp, kemKeyPair, signature)
|
||||
}
|
||||
|
||||
private fun generatePreKeyId(): Int {
|
||||
return SecureRandom().nextInt(Int.MAX_VALUE - 1) + 1
|
||||
}
|
||||
|
||||
private fun generateRegistrationId(): Int {
|
||||
return SecureRandom().nextInt(16380) + 1
|
||||
}
|
||||
|
||||
private fun generateProfileKey(): ProfileKey {
|
||||
val keyBytes = ByteArray(32)
|
||||
SecureRandom().nextBytes(keyBytes)
|
||||
return ProfileKey(keyBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a password for basic auth during registration.
|
||||
* 18 random bytes, base64 encoded with padding.
|
||||
*/
|
||||
private fun generatePassword(): String {
|
||||
val passwordBytes = ByteArray(18)
|
||||
SecureRandom().nextBytes(passwordBytes)
|
||||
return Base64.encodeWithPadding(passwordBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the unidentified access key from a profile key.
|
||||
* This mirrors the logic in UnidentifiedAccess.deriveAccessKeyFrom().
|
||||
*/
|
||||
private fun deriveUnidentifiedAccessKey(profileKey: ProfileKey): ByteArray {
|
||||
val nonce = ByteArray(12)
|
||||
val input = ByteArray(16)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(profileKey.serialize(), "AES"), GCMParameterSpec(128, nonce))
|
||||
|
||||
val ciphertext = cipher.doFinal(input)
|
||||
return ciphertext.copyOf(16)
|
||||
private suspend fun writeRegistrationData(data: RegistrationData) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.filesDir, TEMP_PROTO_FILENAME)
|
||||
file.writeBytes(RegistrationData.ADAPTER.encode(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id("signal-library")
|
||||
id("kotlin-parcelize")
|
||||
id("com.squareup.wire")
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
@@ -20,6 +21,16 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
}
|
||||
|
||||
sourcePath {
|
||||
srcDir("src/main/protowire")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.ui.test.junit4)
|
||||
lintChecks(project(":lintchecks"))
|
||||
|
||||
@@ -10,12 +10,18 @@ import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.registration.NetworkController.AccountAttributes
|
||||
import org.signal.registration.NetworkController.CreateSessionError
|
||||
import org.signal.registration.NetworkController.MasterKeyResponse
|
||||
@@ -29,8 +35,14 @@ import org.signal.registration.NetworkController.RestoreMasterKeyError
|
||||
import org.signal.registration.NetworkController.SessionMetadata
|
||||
import org.signal.registration.NetworkController.SvrCredentials
|
||||
import org.signal.registration.NetworkController.UpdateSessionError
|
||||
import org.signal.registration.proto.ProvisioningData
|
||||
import org.signal.registration.proto.SvrCredential
|
||||
import org.signal.registration.util.SensitiveLog
|
||||
import java.security.SecureRandom
|
||||
import java.util.Locale
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class RegistrationRepository(val context: Context, val networkController: NetworkController, val storageController: StorageController) {
|
||||
|
||||
@@ -98,14 +110,17 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
suspend fun getSvrCredentials(): RegistrationNetworkResult<SvrCredentials, NetworkController.GetSvrCredentialsError> = withContext(Dispatchers.IO) {
|
||||
networkController.getSvrCredentials().also {
|
||||
if (it is RegistrationNetworkResult.Success) {
|
||||
storageController.appendSvrCredentials(listOf(it.data))
|
||||
storageController.updateInProgressRegistrationData {
|
||||
svrCredentials = svrCredentials + SvrCredential(username = it.data.username, password = it.data.password)
|
||||
}
|
||||
BackupManager(context).dataChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRestoredSvrCredentials(): List<SvrCredentials> = withContext(Dispatchers.IO) {
|
||||
storageController.getRestoredSvrCredentials()
|
||||
val data = storageController.readInProgressRegistrationData()
|
||||
data.svrCredentials.map { SvrCredentials(username = it.username, password = it.password) }
|
||||
}
|
||||
|
||||
suspend fun checkSvrCredentials(e164: String, credentials: List<SvrCredentials>): RegistrationNetworkResult<NetworkController.CheckSvrCredentialsResponse, NetworkController.CheckSvrCredentialsError> = withContext(Dispatchers.IO) {
|
||||
@@ -123,9 +138,14 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
pin = pin
|
||||
).also {
|
||||
if (it is RegistrationNetworkResult.Success) {
|
||||
// TODO consider whether we should save this now, or whether we should keep in app state and then hand it back to the library user at the end of the flow
|
||||
storageController.saveValidatedPinAndTemporaryMasterKey(pin, isAlphanumeric, it.data.masterKey, forRegistrationLock)
|
||||
storageController.appendSvrCredentials(listOf(svrCredentials))
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.pin = pin
|
||||
this.pinIsAlphanumeric = isAlphanumeric
|
||||
this.temporaryMasterKey = it.data.masterKey.serialize().toByteString()
|
||||
this.registrationLockEnabled = forRegistrationLock
|
||||
this.svrCredentials += SvrCredential(username = svrCredentials.username, password = svrCredentials.password)
|
||||
}
|
||||
storageController.commitRegistrationData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +235,23 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
suspend fun registerAccountWithProvisioningData(
|
||||
provisioningMessage: NetworkController.ProvisioningMessage
|
||||
): RegistrationNetworkResult<Pair<RegisterAccountResponse, KeyMaterial>, RegisterAccountError> = withContext(Dispatchers.IO) {
|
||||
storageController.saveProvisioningData(provisioningMessage)
|
||||
storageController.updateInProgressRegistrationData {
|
||||
provisioningData = ProvisioningData(
|
||||
restoreMethodToken = provisioningMessage.restoreMethodToken,
|
||||
platform = when (provisioningMessage.platform) {
|
||||
NetworkController.ProvisioningMessage.Platform.ANDROID -> ProvisioningData.Platform.ANDROID
|
||||
NetworkController.ProvisioningMessage.Platform.IOS -> ProvisioningData.Platform.IOS
|
||||
},
|
||||
tier = when (provisioningMessage.tier) {
|
||||
NetworkController.ProvisioningMessage.Tier.FREE -> ProvisioningData.Tier.FREE
|
||||
NetworkController.ProvisioningMessage.Tier.PAID -> ProvisioningData.Tier.PAID
|
||||
null -> ProvisioningData.Tier.TIER_UNKNOWN
|
||||
},
|
||||
backupTimestampMs = provisioningMessage.backupTimestampMs ?: 0,
|
||||
backupSizeBytes = provisioningMessage.backupSizeBytes ?: 0,
|
||||
backupVersion = provisioningMessage.backupVersion
|
||||
)
|
||||
}
|
||||
|
||||
val aep = AccountEntropyPool(provisioningMessage.accountEntropyPool)
|
||||
val recoveryPassword = aep.deriveMasterKey().deriveRegistrationRecoveryPassword()
|
||||
@@ -263,11 +299,26 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
|
||||
Log.i(TAG, "[registerAccount] Starting registration for $e164. sessionId: ${sessionId != null}, recoveryPassword: ${recoveryPassword != null}, registrationLock: ${registrationLock != null}, skipDeviceTransfer: $skipDeviceTransfer, existingAep: ${existingAccountEntropyPool != null}")
|
||||
|
||||
val keyMaterial = storageController.generateAndStoreKeyMaterial(
|
||||
val keyMaterial = generateKeyMaterial(
|
||||
existingAccountEntropyPool = existingAccountEntropyPool,
|
||||
existingAciIdentityKeyPair = existingAciIdentityKeyPair,
|
||||
existingPniIdentityKeyPair = existingPniIdentityKeyPair
|
||||
)
|
||||
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.aciIdentityKeyPair = keyMaterial.aciIdentityKeyPair.serialize().toByteString()
|
||||
this.pniIdentityKeyPair = keyMaterial.pniIdentityKeyPair.serialize().toByteString()
|
||||
this.aciSignedPreKey = keyMaterial.aciSignedPreKey.serialize().toByteString()
|
||||
this.pniSignedPreKey = keyMaterial.pniSignedPreKey.serialize().toByteString()
|
||||
this.aciLastResortKyberPreKey = keyMaterial.aciLastResortKyberPreKey.serialize().toByteString()
|
||||
this.pniLastResortKyberPreKey = keyMaterial.pniLastResortKyberPreKey.serialize().toByteString()
|
||||
this.aciRegistrationId = keyMaterial.aciRegistrationId
|
||||
this.pniRegistrationId = keyMaterial.pniRegistrationId
|
||||
this.unidentifiedAccessKey = keyMaterial.unidentifiedAccessKey.toByteString()
|
||||
this.servicePassword = keyMaterial.servicePassword
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
|
||||
val fcmToken = networkController.getFcmToken()
|
||||
|
||||
val newMasterKey = keyMaterial.accountEntropyPool.deriveMasterKey()
|
||||
@@ -321,15 +372,14 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
)
|
||||
|
||||
if (result is RegistrationNetworkResult.Success) {
|
||||
storageController.saveNewRegistrationData(
|
||||
NewRegistrationData(
|
||||
e164 = result.data.e164,
|
||||
aci = ACI.parseOrThrow(result.data.aci),
|
||||
pni = PNI.parseOrThrow(result.data.pni),
|
||||
servicePassword = keyMaterial.servicePassword,
|
||||
aep = keyMaterial.accountEntropyPool
|
||||
)
|
||||
)
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.e164 = result.data.e164
|
||||
this.aci = result.data.aci
|
||||
this.pni = result.data.pni
|
||||
this.servicePassword = keyMaterial.servicePassword
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
storageController.commitRegistrationData()
|
||||
}
|
||||
|
||||
result.mapSuccess { it to keyMaterial }
|
||||
@@ -343,10 +393,14 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
val result = networkController.setPinAndMasterKeyOnSvr(pin, masterKey)
|
||||
|
||||
if (result is RegistrationNetworkResult.Success) {
|
||||
storageController.saveNewlyCreatedPin(pin, isAlphanumeric)
|
||||
result.data?.let { credential ->
|
||||
storageController.appendSvrCredentials(listOf(credential))
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.pin = pin
|
||||
this.pinIsAlphanumeric = isAlphanumeric
|
||||
result.data?.let { credential ->
|
||||
this.svrCredentials += SvrCredential(username = credential.username, password = credential.password)
|
||||
}
|
||||
}
|
||||
storageController.commitRegistrationData()
|
||||
}
|
||||
|
||||
result
|
||||
@@ -356,6 +410,82 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
return storageController.getPreExistingRegistrationData()
|
||||
}
|
||||
|
||||
private fun generateKeyMaterial(
|
||||
existingAccountEntropyPool: AccountEntropyPool? = null,
|
||||
existingAciIdentityKeyPair: IdentityKeyPair? = null,
|
||||
existingPniIdentityKeyPair: IdentityKeyPair? = null
|
||||
): KeyMaterial {
|
||||
val accountEntropyPool = existingAccountEntropyPool ?: AccountEntropyPool.generate()
|
||||
val aciIdentityKeyPair = existingAciIdentityKeyPair ?: IdentityKeyPair.generate()
|
||||
val pniIdentityKeyPair = existingPniIdentityKeyPair ?: IdentityKeyPair.generate()
|
||||
|
||||
val timestamp = System.currentTimeMillis()
|
||||
|
||||
val aciSignedPreKey = generateSignedPreKey(generatePreKeyId(), timestamp, aciIdentityKeyPair)
|
||||
val pniSignedPreKey = generateSignedPreKey(generatePreKeyId(), timestamp, pniIdentityKeyPair)
|
||||
val aciLastResortKyberPreKey = generateKyberPreKey(generatePreKeyId(), timestamp, aciIdentityKeyPair)
|
||||
val pniLastResortKyberPreKey = generateKyberPreKey(generatePreKeyId(), timestamp, pniIdentityKeyPair)
|
||||
|
||||
val profileKey = generateProfileKey()
|
||||
|
||||
return KeyMaterial(
|
||||
aciIdentityKeyPair = aciIdentityKeyPair,
|
||||
aciSignedPreKey = aciSignedPreKey,
|
||||
aciLastResortKyberPreKey = aciLastResortKyberPreKey,
|
||||
pniIdentityKeyPair = pniIdentityKeyPair,
|
||||
pniSignedPreKey = pniSignedPreKey,
|
||||
pniLastResortKyberPreKey = pniLastResortKyberPreKey,
|
||||
aciRegistrationId = generateRegistrationId(),
|
||||
pniRegistrationId = generateRegistrationId(),
|
||||
unidentifiedAccessKey = deriveUnidentifiedAccessKey(profileKey),
|
||||
servicePassword = generatePassword(),
|
||||
accountEntropyPool = accountEntropyPool
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateSignedPreKey(id: Int, timestamp: Long, identityKeyPair: IdentityKeyPair): SignedPreKeyRecord {
|
||||
val keyPair = ECKeyPair.generate()
|
||||
val signature = identityKeyPair.privateKey.calculateSignature(keyPair.publicKey.serialize())
|
||||
return SignedPreKeyRecord(id, timestamp, keyPair, signature)
|
||||
}
|
||||
|
||||
private fun generateKyberPreKey(id: Int, timestamp: Long, identityKeyPair: IdentityKeyPair): KyberPreKeyRecord {
|
||||
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
|
||||
val signature = identityKeyPair.privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
|
||||
return KyberPreKeyRecord(id, timestamp, kemKeyPair, signature)
|
||||
}
|
||||
|
||||
private fun generatePreKeyId(): Int {
|
||||
return SecureRandom().nextInt(Int.MAX_VALUE - 1) + 1
|
||||
}
|
||||
|
||||
private fun generateRegistrationId(): Int {
|
||||
return SecureRandom().nextInt(16380) + 1
|
||||
}
|
||||
|
||||
private fun generateProfileKey(): ProfileKey {
|
||||
val keyBytes = ByteArray(32)
|
||||
SecureRandom().nextBytes(keyBytes)
|
||||
return ProfileKey(keyBytes)
|
||||
}
|
||||
|
||||
private fun generatePassword(): String {
|
||||
val passwordBytes = ByteArray(18)
|
||||
SecureRandom().nextBytes(passwordBytes)
|
||||
return Base64.encodeWithPadding(passwordBytes)
|
||||
}
|
||||
|
||||
private fun deriveUnidentifiedAccessKey(profileKey: ProfileKey): ByteArray {
|
||||
val nonce = ByteArray(12)
|
||||
val input = ByteArray(16)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(profileKey.serialize(), "AES"), GCMParameterSpec(128, nonce))
|
||||
|
||||
val ciphertext = cipher.doFinal(input)
|
||||
return ciphertext.copyOf(16)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationRepository::class)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.TypeParceler
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.util.ACIParceler
|
||||
import org.signal.registration.util.AccountEntropyPoolParceler
|
||||
import org.signal.registration.util.IdentityKeyPairParceler
|
||||
@@ -22,31 +22,22 @@ import org.signal.registration.util.KyberPreKeyRecordParceler
|
||||
import org.signal.registration.util.PNIParceler
|
||||
import org.signal.registration.util.SignedPreKeyRecordParceler
|
||||
|
||||
/**
|
||||
* The set of methods that the registration module needs to persist data to disk.
|
||||
*
|
||||
* Note that most data is stored via "in progress registration data", which gives the registration module
|
||||
* a lot of control over what data is saved, with the app just needing to persist the blob.
|
||||
*
|
||||
* It's referred to as "in progress" because it represents state that the registration module wants to persist
|
||||
* in case the process were to die, but it's not fully ready to be committed as permanent app state yet.
|
||||
*
|
||||
* For example, the module may create a bunch of keys, but until the user is registered and those keys are uploaded,
|
||||
* they should not be considered the actual keys for the current account.
|
||||
*
|
||||
* When the data *is* ready to be committed, it will be done via [commitRegistrationData].
|
||||
*/
|
||||
interface StorageController {
|
||||
|
||||
/**
|
||||
* Generates all key material required for account registration and stores it persistently.
|
||||
* This includes ACI identity key, PNI identity key, and their respective pre-keys.
|
||||
*
|
||||
* If optional parameters are provided (e.g. from a pre-existing registration), those values
|
||||
* will be re-used instead of generating new ones.
|
||||
*
|
||||
* @param existingAccountEntropyPool If non-null, re-use this AEP instead of generating a new one.
|
||||
* @param existingAciIdentityKeyPair If non-null, re-use this ACI identity key pair instead of generating a new one.
|
||||
* @param existingPniIdentityKeyPair If non-null, re-use this PNI identity key pair instead of generating a new one.
|
||||
* @return [KeyMaterial] containing all generated cryptographic material needed for registration.
|
||||
*/
|
||||
suspend fun generateAndStoreKeyMaterial(
|
||||
existingAccountEntropyPool: AccountEntropyPool? = null,
|
||||
existingAciIdentityKeyPair: IdentityKeyPair? = null,
|
||||
existingPniIdentityKeyPair: IdentityKeyPair? = null
|
||||
): KeyMaterial
|
||||
|
||||
/**
|
||||
* Called after a successful registration to store new registration data.
|
||||
*/
|
||||
suspend fun saveNewRegistrationData(newRegistrationData: NewRegistrationData)
|
||||
|
||||
/**
|
||||
* Retrieves previously stored registration data for registered installs, if any.
|
||||
*
|
||||
@@ -54,50 +45,38 @@ interface StorageController {
|
||||
*/
|
||||
suspend fun getPreExistingRegistrationData(): PreExistingRegistrationData?
|
||||
|
||||
/**
|
||||
* Retrieves any SVR2 credentials that may have been restored via the OS-level backup/restore service. May be empty.
|
||||
*/
|
||||
suspend fun getRestoredSvrCredentials(): List<NetworkController.SvrCredentials>
|
||||
|
||||
// TODO [regV5] Can this just take a single item?
|
||||
/**
|
||||
* Appends known-working SVR credentials to the local store of credentials.
|
||||
* Implementations should limit the number of stored credentials to some reasonable maximum.
|
||||
*/
|
||||
suspend fun appendSvrCredentials(credentials: List<NetworkController.SvrCredentials>)
|
||||
|
||||
/**
|
||||
* Saves a validated PIN, temporary master key, and registration lock status.
|
||||
*
|
||||
* Called after successfully verifying a PIN against SVR, either during
|
||||
* registration lock unlock or SVR restore flows.
|
||||
*
|
||||
* It's a "temporary master key" because at the end of the day, what we actually want is a master key derived from the AEP.
|
||||
* We may need this master key to perform the initial storage service restore, but after that's done, it will be discarded after generating a new AEP.
|
||||
*
|
||||
* @param pin The validated PIN that was successfully verified.
|
||||
* @param registrationLockEnabled Whether registration lock should be enabled for this account.
|
||||
*/
|
||||
suspend fun saveValidatedPinAndTemporaryMasterKey(pin: String, isAlphanumeric: Boolean, masterKey: MasterKey, registrationLockEnabled: Boolean)
|
||||
|
||||
/**
|
||||
* Saves a newly-created PIN for the account.
|
||||
*/
|
||||
suspend fun saveNewlyCreatedPin(pin: String, isAlphanumeric: Boolean)
|
||||
|
||||
/**
|
||||
* Saves metadata from a provisioning message received during QR-based restore.
|
||||
*
|
||||
* This includes the restore method token, backup tier, backup timestamps, and
|
||||
* platform information from the old device. Called before registering with
|
||||
* the provisioned data.
|
||||
*/
|
||||
suspend fun saveProvisioningData(provisioningMessage: NetworkController.ProvisioningMessage)
|
||||
|
||||
/**
|
||||
* Clears all stored registration data, including key material and account information.
|
||||
*/
|
||||
suspend fun clearAllData()
|
||||
|
||||
/**
|
||||
* Reads the persisted [RegistrationData] proto that is currently in the process of being worked on.
|
||||
* Returns a default empty [RegistrationData] if nothing has been written yet.
|
||||
*/
|
||||
suspend fun readInProgressRegistrationData(): RegistrationData
|
||||
|
||||
/**
|
||||
* Reads the persisted [RegistrationData] (that is currently in the process of being worked on),
|
||||
* applies the [updater] to its builder, and writes the result back to persistent storage.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* storageController.updateRegistrationData {
|
||||
* pin = "1234"
|
||||
* pinIsAlphanumeric = false
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
suspend fun updateInProgressRegistrationData(updater: RegistrationData.Builder.() -> Unit)
|
||||
|
||||
/**
|
||||
* Commits in-progress [RegistrationData] to permanent storage. Any data in the blob should be considered actual data
|
||||
* for the currently-registered account. Commits can happen multiple times. For instance, we will commit data right after
|
||||
* successfully registering, but then there may be more operations we perform after registration that need to be
|
||||
* separately committed.
|
||||
*/
|
||||
suspend fun commitRegistrationData()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
68
feature/registration/src/main/protowire/Registration.proto
Normal file
68
feature/registration/src/main/protowire/Registration.proto
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.signal.registration.proto";
|
||||
|
||||
message RegistrationData {
|
||||
// Key material (from generateAndStoreKeyMaterial)
|
||||
bytes aciIdentityKeyPair = 1;
|
||||
bytes pniIdentityKeyPair = 2;
|
||||
bytes aciSignedPreKey = 3;
|
||||
bytes pniSignedPreKey = 4;
|
||||
bytes aciLastResortKyberPreKey = 5;
|
||||
bytes pniLastResortKyberPreKey = 6;
|
||||
int32 aciRegistrationId = 7;
|
||||
int32 pniRegistrationId = 8;
|
||||
bytes unidentifiedAccessKey = 9;
|
||||
string servicePassword = 10;
|
||||
string accountEntropyPool = 11;
|
||||
|
||||
// Account identity (from saveNewRegistrationData / getPreExistingRegistrationData)
|
||||
string e164 = 12;
|
||||
string aci = 13;
|
||||
string pni = 14;
|
||||
|
||||
// PIN data (from saveValidatedPinAndTemporaryMasterKey / saveNewlyCreatedPin)
|
||||
string pin = 15;
|
||||
bool pinIsAlphanumeric = 16;
|
||||
bytes temporaryMasterKey = 17;
|
||||
bool registrationLockEnabled = 18;
|
||||
|
||||
// SVR credentials (from appendSvrCredentials / getRestoredSvrCredentials)
|
||||
repeated SvrCredential svrCredentials = 19;
|
||||
|
||||
// Provisioning data (from saveProvisioningData)
|
||||
ProvisioningData provisioningData = 20;
|
||||
}
|
||||
|
||||
message SvrCredential {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message ProvisioningData {
|
||||
enum Platform {
|
||||
UNKNOWN = 0;
|
||||
ANDROID = 1;
|
||||
IOS = 2;
|
||||
}
|
||||
|
||||
enum Tier {
|
||||
TIER_UNKNOWN = 0;
|
||||
FREE = 1;
|
||||
PAID = 2;
|
||||
}
|
||||
|
||||
string restoreMethodToken = 1;
|
||||
Platform platform = 2;
|
||||
Tier tier = 3;
|
||||
int64 backupTimestampMs = 4;
|
||||
int64 backupSizeBytes = 5;
|
||||
int64 backupVersion = 6;
|
||||
}
|
||||
Reference in New Issue
Block a user