story-kit: enforce cryptographic identity verification for Matrix commands (story 246)
Remove the require_verified_devices config toggle. The bot now always requires encrypted rooms and cross-signing-verified devices before executing any command. Messages from unencrypted rooms or unverified devices are rejected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,9 +77,6 @@ pub struct BotContext {
|
||||
/// bot so it can continue a conversation thread without requiring an
|
||||
/// explicit `@mention` on every follow-up.
|
||||
pub bot_sent_event_ids: Arc<TokioMutex<HashSet<OwnedEventId>>>,
|
||||
/// When `true`, the bot rejects messages from users whose devices have not
|
||||
/// been verified via cross-signing in encrypted rooms.
|
||||
pub require_verified_devices: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -192,12 +189,9 @@ pub async fn run_bot(config: BotConfig, project_root: PathBuf) -> Result<(), Str
|
||||
history: Arc::new(TokioMutex::new(HashMap::new())),
|
||||
history_size: config.history_size,
|
||||
bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())),
|
||||
require_verified_devices: config.require_verified_devices,
|
||||
};
|
||||
|
||||
if config.require_verified_devices {
|
||||
slog!("[matrix-bot] require_verified_devices is ON — messages from unverified devices in encrypted rooms will be rejected");
|
||||
}
|
||||
slog!("[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected");
|
||||
|
||||
// Register event handlers and inject shared context.
|
||||
client.add_event_handler_context(ctx);
|
||||
@@ -475,28 +469,37 @@ async fn on_room_message(
|
||||
return;
|
||||
}
|
||||
|
||||
// When require_verified_devices is enabled and the room is encrypted,
|
||||
// reject messages from users whose devices have not been verified.
|
||||
if ctx.require_verified_devices && room.encryption_state().is_encrypted() {
|
||||
match check_sender_verified(&client, &ev.sender).await {
|
||||
Ok(true) => { /* sender has at least one verified device — proceed */ }
|
||||
Ok(false) => {
|
||||
slog!(
|
||||
"[matrix-bot] WARNING: Rejecting message from {} — \
|
||||
unverified device(s) in encrypted room {}",
|
||||
ev.sender,
|
||||
incoming_room_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
slog!(
|
||||
"[matrix-bot] Error checking verification for {}: {e} — \
|
||||
rejecting message (fail-closed)",
|
||||
ev.sender
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Reject commands from unencrypted rooms — E2EE is mandatory.
|
||||
if !room.encryption_state().is_encrypted() {
|
||||
slog!(
|
||||
"[matrix-bot] Rejecting message from {} — room {} is not encrypted. \
|
||||
Commands are only accepted from encrypted rooms.",
|
||||
ev.sender,
|
||||
incoming_room_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always verify that the sender has at least one cross-signing-verified
|
||||
// device. This check is unconditional and cannot be disabled via config.
|
||||
match check_sender_verified(&client, &ev.sender).await {
|
||||
Ok(true) => { /* sender has at least one verified device — proceed */ }
|
||||
Ok(false) => {
|
||||
slog!(
|
||||
"[matrix-bot] Rejecting message from {} — no cross-signing-verified \
|
||||
device found in encrypted room {}",
|
||||
ev.sender,
|
||||
incoming_room_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
slog!(
|
||||
"[matrix-bot] Error checking verification for {}: {e} — \
|
||||
rejecting message (fail-closed)",
|
||||
ev.sender
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,7 +933,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bot_context_require_verified_devices_field() {
|
||||
fn bot_context_has_no_require_verified_devices_field() {
|
||||
// Verification is always on — BotContext no longer has a toggle field.
|
||||
// This test verifies the struct can be constructed and cloned without it.
|
||||
let ctx = BotContext {
|
||||
bot_user_id: make_user_id("@bot:example.com"),
|
||||
target_room_ids: vec![],
|
||||
@@ -939,15 +944,9 @@ mod tests {
|
||||
history: Arc::new(TokioMutex::new(HashMap::new())),
|
||||
history_size: 20,
|
||||
bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())),
|
||||
require_verified_devices: true,
|
||||
};
|
||||
assert!(ctx.require_verified_devices);
|
||||
|
||||
let ctx_off = BotContext {
|
||||
require_verified_devices: false,
|
||||
..ctx
|
||||
};
|
||||
assert!(!ctx_off.require_verified_devices);
|
||||
// Clone must work (required by Matrix SDK event handler injection).
|
||||
let _cloned = ctx.clone();
|
||||
}
|
||||
|
||||
// -- drain_complete_paragraphs ------------------------------------------
|
||||
|
||||
@@ -35,12 +35,6 @@ pub struct BotConfig {
|
||||
/// dropped. Defaults to 20.
|
||||
#[serde(default = "default_history_size")]
|
||||
pub history_size: usize,
|
||||
/// When `true`, the bot rejects messages from users whose devices have not
|
||||
/// been verified via cross-signing in encrypted rooms. When `false`
|
||||
/// (default), messages are accepted regardless of device verification
|
||||
/// status, preserving existing plaintext-room behaviour.
|
||||
#[serde(default)]
|
||||
pub require_verified_devices: bool,
|
||||
/// Previously used to select an Anthropic model. Now ignored — the bot
|
||||
/// uses Claude Code which manages its own model selection. Kept for
|
||||
/// backwards compatibility so existing bot.toml files still parse.
|
||||
@@ -241,47 +235,6 @@ enabled = true
|
||||
assert_eq!(config.history_size, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_defaults_require_verified_devices_to_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".story_kit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert!(!config.require_verified_devices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_respects_require_verified_devices_true() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".story_kit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
require_verified_devices = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert!(config.require_verified_devices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_respects_custom_history_size() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -302,4 +255,32 @@ history_size = 50
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.history_size, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ignores_legacy_require_verified_devices_key() {
|
||||
// Old bot.toml files that still have `require_verified_devices = true`
|
||||
// must parse successfully — the field is simply ignored now that
|
||||
// verification is always enforced unconditionally.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".story_kit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
require_verified_devices = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Should still load successfully despite the unknown field.
|
||||
let config = BotConfig::load(tmp.path());
|
||||
assert!(
|
||||
config.is_some(),
|
||||
"bot.toml with legacy require_verified_devices key must still load"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user