Improve the storage controller for regV5.

This commit is contained in:
Greyson Parrelli
2026-03-16 09:07:04 -04:00
committed by Michelle Tang
parent 6877b9163b
commit d2c8b6e14c
5 changed files with 391 additions and 235 deletions

View File

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

View File

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

View File

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

View File

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

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