diff --git a/host/src/cred/items.rs b/host/src/cred/items.rs index 1649f44..4ba8579 100644 --- a/host/src/cred/items.rs +++ b/host/src/cred/items.rs @@ -295,6 +295,7 @@ impl Cred { config.fields[index].needs_value = false; config.fields[index].field_type = kind; config.fields[index].key_type = key_type; + validate(&config)?; tx.execute( "INSERT INTO versions(item,created,config) VALUES(?1,unixepoch(),?2)", params![item, previous], diff --git a/host/tests/cred_regressions.rs b/host/tests/cred_regressions.rs index ddaf32d..adc8546 100644 --- a/host/tests/cred_regressions.rs +++ b/host/tests/cred_regressions.rs @@ -218,8 +218,61 @@ fn deleted_item_revert_preserves_identity_but_never_restores_secrets() { assert_eq!(restored.fields[0].field_id, item.fields[0].field_id); } #[test] +fn secret_entry_keeps_oauth_cardinality_and_rolls_back_ciphertext() { + let (dir, cred, caller, item) = fixture(); + let input = FieldInput { + value: "opaque", + field_type: Some(types::FieldType::Oauth2RefreshToken), + key_type: None, + }; + cred.set_field_input(&item.id, &item.fields[0].field_id, &input, &caller, "pk") + .unwrap(); + cred.set_field(&item.id, &item.fields[1].field_id, "opaque", &caller, "pk") + .unwrap(); + let current = cred.item_get(&item.id).unwrap(); + let history = cred.item_versions(&item.id).unwrap().len(); + let audit = cred.audit_list().unwrap().len(); + let db = rusqlite::Connection::open(dir.path().join("cred.sqlite")).unwrap(); + let cipher = || { + db.query_row( + "SELECT ciphertext FROM fields WHERE item=?1 AND id=?2", + rusqlite::params![item.id, item.fields[1].field_id], + |r| r.get::<_, Vec>(0), + ) + .unwrap() + }; + let before = cipher(); + assert_eq!( + cred.set_field_input(&item.id, &item.fields[1].field_id, &input, &caller, "pk") + .err() + .unwrap() + .0, + "invalid_params" + ); + assert!(cipher() == before); + assert_eq!(cred.item_get(&item.id).unwrap().version, current.version); + assert_eq!(cred.item_versions(&item.id).unwrap().len(), history); + assert_eq!(cred.audit_list().unwrap().len(), audit); +} +#[test] fn typed_secret_entry_and_bundle_normalization() { let (_dir, cred, caller, item) = fixture(); + let mut metadata = item.clone(); + metadata.fields[0].field_type = types::FieldType::SshPrivateKey; + metadata.fields[0].key_type = Some("es256".into()); + let id = cred + .file_edit_request( + &caller, + "metadata", + Edit { + item: item.id.clone(), + base_version: item.version, + config: metadata, + }, + ) + .unwrap(); + cred.approve(&id, "yes", Overrides::default(), Some("pk")) + .unwrap(); let input = FieldInput { value: "two\nlines", field_type: Some(types::FieldType::Note), @@ -229,6 +282,7 @@ fn typed_secret_entry_and_bundle_normalization() { .unwrap(); let current = cred.item_get(&item.id).unwrap(); assert_eq!(current.fields[0].field_type, types::FieldType::Note); + assert!(current.fields[0].key_type.is_none()); assert!(current.fields[0].value.is_none()); assert_eq!( cred.item_versions(&item.id).unwrap()[0].config.fields[0].field_type,