Removed devices: DeviceRevoked, identity reset and one retry with the ticket
kv-sync: SyncError::DeviceRevoked for 403 device_revoked (drops the session); the test server answers it after OPAQUE (before the ticket) and for valid signatures by a revoked device's key. kv-ffi: CoreError::DeviceRevoked; the foreign DeviceSigner gains reset() (new key and id). On device_revoked the core resets the identity, forgets the session and the kept OPAQUE input (no silent re-enrolment) and keeps the vault data; a login carrying a ticket retries once as the new device. reset_device_identity() runs the reset on demand. Signed calls (sync, devices, tickets, join) reset too. kv-wasm: DeviceRevoked; the browser binding replaces the WebCrypto key and id in IndexedDB (same name and platform), loginWithSetup retries once with a ticket; resetDeviceIdentity(). Tests: end to end revoked -> reset -> re-enrol with the same ticket (kv-ffi and web core), no-ticket path, in-browser IndexedDB reset. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -136,13 +136,13 @@ impl VaultCore {
|
||||
/// The "Add a device" QR: issues a 10-minute, single-use, unbound enrollment ticket and returns the
|
||||
/// payload. Show it as a secret view; on Cancel call `revoke_enrollment_ticket`.
|
||||
pub async fn add_device_payload(&self) -> Result<AddDevicePayload> {
|
||||
let created = self.api()?.create_ticket(None).await?;
|
||||
let created = self.revoked(self.api()?.create_ticket(None).await)?;
|
||||
let payload = self.setup_payload(Some(created.ticket))?.encode()?.to_string();
|
||||
Ok(AddDevicePayload { payload, ticket_id: created.ticket_id.to_string(), expires_at: created.expires_at })
|
||||
}
|
||||
|
||||
pub async fn revoke_enrollment_ticket(&self, ticket_id: String) -> Result<()> {
|
||||
Ok(self.api()?.revoke_ticket(parse_id(&ticket_id)?).await?)
|
||||
self.revoked(self.api()?.revoke_ticket(parse_id(&ticket_id)?).await)
|
||||
}
|
||||
|
||||
/// New device: configures the server and pins from `setup`, then logs in with the master password and the
|
||||
@@ -243,7 +243,7 @@ impl VaultCore {
|
||||
}
|
||||
self.unlocked_snapshot()?;
|
||||
let keypair = PairingKeypair::generate();
|
||||
let hello = api.join_hello(&qr.rendezvous_id, keypair.public_bytes().to_vec()).await?;
|
||||
let hello = self.revoked(api.join_hello(&qr.rendezvous_id, keypair.public_bytes().to_vec()).await)?;
|
||||
let rid_bytes = URL_SAFE_NO_PAD.decode(&qr.rendezvous_id).map_err(internal)?;
|
||||
let confirm = JoinConfirm {
|
||||
code: pairing::sas(&rid_bytes, &qr.public_key, &keypair.public_bytes()),
|
||||
@@ -262,11 +262,11 @@ impl VaultCore {
|
||||
return Err(invalid("no join to approve"));
|
||||
};
|
||||
let api = self.api()?;
|
||||
let ticket = api.create_ticket(Some(Sha256::digest(&device.p256_pk).to_vec())).await?;
|
||||
let ticket = self.revoked(api.create_ticket(Some(Sha256::digest(&device.p256_pk).to_vec())).await)?;
|
||||
let payload = self.setup_payload(Some(ticket.ticket))?.encode()?;
|
||||
let rid_bytes = URL_SAFE_NO_PAD.decode(&qr.rendezvous_id).map_err(internal)?;
|
||||
let bundle = pairing::seal(&keypair, &qr.public_key, qr.qr_secret.as_ref(), &rid_bytes, payload.as_bytes())?;
|
||||
Ok(api.join_send(&qr.rendezvous_id, bundle).await?)
|
||||
self.revoked(api.join_send(&qr.rendezvous_id, bundle).await)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user