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:
@@ -32,6 +32,9 @@ impl DeviceSigner for Signer {
|
||||
fn sign(&self, message: Vec<u8>) -> Result<Vec<u8>, kv_ffi::CoreError> {
|
||||
kv_sync::DeviceSigner::sign(&self.0, &message).map_err(|e| kv_ffi::CoreError::Internal { msg: e.to_string() })
|
||||
}
|
||||
fn reset(&self) -> Result<(), kv_ffi::CoreError> {
|
||||
Err(kv_ffi::CoreError::Internal { msg: "not supported in this test".into() })
|
||||
}
|
||||
}
|
||||
|
||||
fn web_item(title: &str, password: &str) -> ItemDto {
|
||||
@@ -371,3 +374,46 @@ async fn web_vault_join_refuses_a_swapped_approver() {
|
||||
web.join_cancel();
|
||||
assert!(matches!(web.join_poll().await, Err(CoreError::Invalid { .. })));
|
||||
}
|
||||
|
||||
/// docs/03 "Removed devices" for the web core: a removed browser gets `DeviceRevoked` (only with the right
|
||||
/// password), forgets its session, and the same "Add a device" ticket gets it back in under a new key and id
|
||||
/// (what the browser binding does: `forget_revoked_device`, new WebCrypto key, one retry).
|
||||
#[tokio::test]
|
||||
async fn removed_web_vault_reenrols_with_the_same_ticket() {
|
||||
let server = TestServer::start().await;
|
||||
let (app, _sk, id) = app_with_item(&server).await;
|
||||
let add = app.add_device_payload().await.unwrap();
|
||||
let setup: kv_wasm::dto::SetupInfo = {
|
||||
let i = kv_ffi::parse_setup_payload(add.payload.clone()).unwrap();
|
||||
kv_wasm::dto::SetupInfo {
|
||||
server_url: i.server_url,
|
||||
spki_pins: i.spki_pins,
|
||||
account_id: i.account_id,
|
||||
secret_key: i.secret_key,
|
||||
ticket: i.ticket,
|
||||
}
|
||||
};
|
||||
|
||||
// The browser enrolled once (pending), then was denied.
|
||||
let (store, web) = new_web(&server);
|
||||
let old = web.device_id().unwrap();
|
||||
let no_ticket = kv_wasm::dto::SetupInfo { ticket: None, ..setup.clone() };
|
||||
assert_eq!(web.login_with_setup(&no_ticket, PW).await.unwrap(), LoginResult::PendingApproval);
|
||||
app.revoke_device(old.to_string()).await.unwrap();
|
||||
|
||||
assert!(matches!(web.login_with_setup(&setup, "wrong password!!").await, Err(CoreError::BadCredentials)));
|
||||
assert!(matches!(web.login_with_setup(&setup, PW).await, Err(CoreError::DeviceRevoked)));
|
||||
web.forget_revoked_device().unwrap();
|
||||
web.set_device_signer(Arc::new(SoftwareSigner::generate(Uuid::new_v4())), "Web vault", Platform::Macos);
|
||||
assert_ne!(web.device_id().unwrap(), old);
|
||||
assert_eq!(web.login_with_setup(&setup, PW).await.unwrap(), LoginResult::Ok);
|
||||
web.sync_now().await.unwrap();
|
||||
assert_eq!(web.get_item(&id).unwrap().password.as_deref(), Some("from the app"));
|
||||
drop(store);
|
||||
|
||||
// Signed in, then removed: sync says so; the old id never comes back.
|
||||
app.revoke_device(web.device_id().unwrap().to_string()).await.unwrap();
|
||||
assert!(matches!(web.sync_now().await, Err(CoreError::DeviceRevoked)));
|
||||
web.forget_revoked_device().unwrap();
|
||||
assert!(matches!(app.approve_device(old.to_string()).await, Err(kv_ffi::CoreError::NotFound)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user