b7c31dd4fa00
fix: gate vault adoption on both confirmations (v2.3.77)
- time
- 2026-07-26 04:47 UTC · 3 months ago
- machine
- koh2
- parent
- bdf1f16e8bcf
↓ peach-android-b7c31dd4.tar.gzatlas at this save
files changed3 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/sync/ChirpAdopt.kt+23 −0app/src/test/java/com/peachpasswords/sync/ChirpAdoptTest.kt+33 −2app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23760020- versionName "2.3.76"19+ versionCode 23770020+ versionName "2.3.77"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/sync/ChirpAdopt.kt+23 −0
@@ -39,6 +39,12 @@3939 ) : AdoptionFrame()4040 4141 @Serializable42+ @SerialName("ADOPT_CONFIRM")43+ data class Confirm(44+ val confirmed: Boolean,45+ ) : AdoptionFrame()46+47+ @Serializable4248 @SerialName("ADOPT_GRANT")4349 data class Grant(4450 @SerialName("root_key_hex") val rootKeyHex: String,@@ -73,6 +79,8 @@7379 val deviceName: String7480 val deviceType: DeviceType7581 fun generateDeviceId(): String82+ /** Root-key transfer cannot begin until this device confirms the SAS. */83+ suspend fun confirmSas(): Boolean7684 7785 /** Seal a fresh local vault under the receiver's password, reusing [rootKeyHex]. Returns the vault id. */7886 suspend fun createVaultFromRootKey(rootKeyHex: String): String@@ -115,6 +123,9 @@115123 isDeviceType(frame.source.deviceType) &&116124 Hex64.matches(frame.source.longTermPublicKeyHex)117125 126+fun isAdoptConfirm(frame: AdoptionFrame?): Boolean =127+ frame is AdoptionFrame.Confirm128+118129 fun isAdoptIdentity(frame: AdoptionFrame?): Boolean =119130 frame is AdoptionFrame.Identity &&120131 frame.deviceId.isNotEmpty() &&@@ -141,6 +152,13 @@141152 deviceType = deps.deviceType.wireValue,142153 ))143154 155+ val confirmed = deps.confirmSas()156+ sendFrame(channel, AdoptionFrame.Confirm(confirmed))157+ if (!confirmed) {158+ deps.onError("ADOPT_SAS_REJECTED")159+ return null160+ }161+144162 val frame = receiveFrame(channel)145163 if (!isAdoptGrant(frame)) {146164 deps.onError("ADOPT_UNEXPECTED_FRAME")@@ -201,6 +219,11 @@201219 nowMs: () -> Long = { System.currentTimeMillis() },202220 ): PairedDevice? {203221 if (!deps.onConsent(request)) return null222+223+ val confirmation = receiveFrame(channel)224+ if (!isAdoptConfirm(confirmation) || !(confirmation as AdoptionFrame.Confirm).confirmed) {225+ return null226+ }204227 205228 val rootKeyHex = deps.rootKeyHex()206229 if (!validateRootKeyHex(rootKeyHex)) throw IllegalStateException("ADOPT_INVALID_ROOT_KEY")app/src/test/java/com/peachpasswords/sync/ChirpAdoptTest.kt+33 −2
@@ -56,12 +56,14 @@5656 private val vaultId: String?,5757 private val createThrows: Boolean = false,5858 private val derived: LocalChirpIdentity? = null,59+ private val sasConfirmed: Boolean = true,5960 ) : AdoptionReceiverDeps {6061 var createCalledWith: String? = null6162 var licenseApplied: String? = null6263 val upserted = mutableListOf<PairedDevice>()6364 val errors = mutableListOf<String>()6465 override fun generateDeviceId(): String = "receiver-ephemeral"66+ override suspend fun confirmSas(): Boolean = sasConfirmed6567 override suspend fun createVaultFromRootKey(rootKeyHex: String): String {6668 createCalledWith = rootKeyHex6769 if (createThrows) throw IllegalStateException("commit_failed")@@ -82,7 +84,11 @@8284 ) : AdoptionSourceDeps {8385 val upserted = mutableListOf<PairedDevice>()8486 var consentAsked = false85- override suspend fun rootKeyHex(): String = rootKeyHex87+ var rootRequested = false88+ override suspend fun rootKeyHex(): String {89+ rootRequested = true90+ return rootKeyHex91+ }8692 override suspend fun vaultId(): String = vaultId8793 override suspend fun license(): String? = license8894 override suspend fun onConsent(request: AdoptionFrame.Request): Boolean {@@ -129,6 +135,7 @@129135 val request = AdoptionFrame.Request("ephemeral-label-not-an-identity", "Receiver", "extension")130136 131137 val grantJob = async { grantAdoption(sourceCh, request, source) { 0L } }138+ sendFrame(receiverCh, AdoptionFrame.Confirm(true))132139 receiveFrame(receiverCh) // the grant133140 sendFrame(134141 receiverCh,@@ -176,10 +183,33 @@176183 177184 assertNull(granted)178185 assertTrue(source.consentAsked)186+ assertFalse(source.rootRequested)179187 assertTrue(source.upserted.isEmpty())180188 assertNull(receiver.createCalledWith)181189 }182190 191+ @Test fun receiver_sas_rejection_grants_no_root_key() = runBlocking {192+ val (receiverCh, sourceCh) = memoryPair()193+ val rootKeyHex = "66".repeat(32)194+ val receiver = RecorderReceiver(195+ "Receiver",196+ DeviceType.ANDROID,197+ vaultIdFor(rootKeyHex),198+ sasConfirmed = false,199+ )200+ val source = RecorderSource(identity("source"), rootKeyHex, vaultIdFor(rootKeyHex))201+202+ val receiverJob = async { requestAdoption(receiverCh, receiver) { 0L } }203+ val request = receiveFrame(sourceCh) as AdoptionFrame.Request204+ val sourceJob = async { grantAdoption(sourceCh, request, source) { 0L } }205+206+ assertNull(receiverJob.await())207+ assertNull(sourceJob.await())208+ assertFalse(source.rootRequested)209+ assertNull(receiver.createCalledWith)210+ assertEquals(listOf("ADOPT_SAS_REJECTED"), receiver.errors)211+ }212+183213 @Test fun invalid_root_key_hex_aborts_as_unexpected_frame() = runBlocking {184214 val (receiverCh, sourceCh) = memoryPair()185215 val receiver = RecorderReceiver("Receiver", DeviceType.ANDROID, vaultId = null)@@ -213,11 +243,12 @@213243 }214244 215245 @Test fun source_waits_for_identity_before_persisting_receiver() = runBlocking {216- val (_, sourceCh) = memoryPair()246+ val (receiverCh, sourceCh) = memoryPair()217247 val rootKeyHex = "33".repeat(32)218248 val source = RecorderSource(identity("source"), rootKeyHex, vaultIdFor(rootKeyHex))219249 val request = AdoptionFrame.Request("receiver-ephemeral", "Receiver", "extension")220250 251+ sendFrame(receiverCh, AdoptionFrame.Confirm(true))221252 val result = withTimeoutOrNull(150) {222253 // No receiver answers the ADOPT_IDENTITY read, so grant must hang, not persist.223254 grantAdoption(sourceCh, request, source) { 0L }