Compare commits

...
6 changed files with 325 additions and 18 deletions

240
Cargo.lock generated
View file

@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "accessory"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "adler2"
version = "2.0.1"
@ -373,6 +385,15 @@ dependencies = [
"cipher 0.5.2",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@ -648,6 +669,15 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@ -773,6 +803,18 @@ dependencies = [
"regex",
]
[[package]]
name = "delegate-display"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "der"
version = "0.7.10"
@ -927,6 +969,18 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy_constructor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2"
dependencies = [
"macroific",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fastrand"
version = "2.4.1"
@ -1136,6 +1190,19 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "growable-bloom-filter"
version = "2.1.1"
@ -1682,6 +1749,23 @@ dependencies = [
"quote",
]
[[package]]
name = "indexed_db_futures"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c"
dependencies = [
"accessory",
"cfg-if",
"delegate-display",
"fancy_constructor",
"js-sys",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@ -1883,6 +1967,53 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "macroific"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd"
dependencies = [
"macroific_attr_parse",
"macroific_core",
"macroific_macro",
]
[[package]]
name = "macroific_attr_parse"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "macroific_core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "macroific_macro"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae"
dependencies = [
"macroific_attr_parse",
"macroific_core",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "maplit"
version = "1.0.2"
@ -1982,6 +2113,7 @@ dependencies = [
"language-tags",
"matrix-sdk-base",
"matrix-sdk-common",
"matrix-sdk-indexeddb",
"matrix-sdk-sqlite",
"mime",
"mime2ext",
@ -2022,6 +2154,7 @@ dependencies = [
"futures-util",
"growable-bloom-filter",
"matrix-sdk-common",
"matrix-sdk-crypto",
"matrix-sdk-store-encryption",
"once_cell",
"regex",
@ -2054,9 +2187,81 @@ dependencies = [
"tracing",
"tracing-subscriber",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "matrix-sdk-crypto"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61bf6c3195de301c98339283413a4e9d9d63c4f214ef8955147643caab161256"
dependencies = [
"aes",
"aquamarine",
"as_variant",
"async-trait",
"bs58",
"byteorder",
"cfg-if",
"ctr",
"eyeball",
"futures-core",
"futures-util",
"hkdf",
"hmac",
"itertools 0.14.0",
"js_option",
"matrix-sdk-common",
"pbkdf2",
"rand 0.8.6",
"rmp-serde",
"ruma",
"serde",
"serde_json",
"sha2",
"subtle",
"thiserror 2.0.18",
"time",
"tokio",
"tokio-stream",
"tracing",
"ulid",
"url",
"vodozemac",
"zeroize",
]
[[package]]
name = "matrix-sdk-indexeddb"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2752015e69b6b56a8df72e52f91a834771259f755eb1c65232b77348ffb16b"
dependencies = [
"anyhow",
"async-trait",
"base64",
"getrandom 0.2.17",
"gloo-utils",
"hkdf",
"indexed_db_futures",
"js-sys",
"matrix-sdk-crypto",
"matrix-sdk-store-encryption",
"rmp-serde",
"ruma",
"serde",
"serde-wasm-bindgen",
"serde_json",
"sha2",
"thiserror 2.0.18",
"tokio",
"tracing",
"wasm-bindgen",
"web-sys",
"zeroize",
]
[[package]]
name = "matrix-sdk-sqlite"
version = "0.14.0"
@ -2068,6 +2273,7 @@ dependencies = [
"deadpool-sqlite",
"itertools 0.14.0",
"matrix-sdk-base",
"matrix-sdk-crypto",
"matrix-sdk-store-encryption",
"num_cpus",
"rmp-serde",
@ -2854,6 +3060,7 @@ dependencies = [
"getrandom 0.2.17",
"http",
"indexmap",
"js-sys",
"js_int",
"konst",
"percent-encoding",
@ -3112,6 +3319,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
@ -3465,10 +3683,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
@ -3477,6 +3697,16 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@ -3801,6 +4031,16 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
[[package]]
name = "ulid"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
dependencies = [
"rand 0.9.4",
"web-time",
]
[[package]]
name = "unicase"
version = "2.9.0"

View file

@ -63,5 +63,6 @@ matrix-sdk = { version = "0.14", default-features = false, features = [
"rustls-tls",
"sqlite",
"markdown",
"e2e-encryption",
] }
futures-util = "0.3"

View file

@ -119,8 +119,22 @@ Initial rollout settings:
engage). The absent
`yes_i_am_very_very_sure_…_open_registration_…` flag keeps the
server closed to anyone without the token.
- `allow_encryption = false` per operator call; E2EE re-enabling is
deferred to a follow-up.
- `allow_encryption` — server-side E2EE switch, sourced from
`services.hyperhive.matrix.allowEncryption` (**default `false`**, opt-in).
Off by default because on the hive-internal homeserver the operator
already controls the transport; turn it on for encrypted rooms on
external / federated homeservers or to keep contents opaque to the
homeserver admin. **The agent matrix client always supports decryption
regardless of this flag** — it uses the `e2e-encryption` feature of
`matrix-sdk` so it can read encrypted rooms it is invited to even when
this homeserver doesn't permit room encryption. Crypto keys are stored
in the per-agent sqlite store under the state dir and persist across
restarts (lost on `--purge`). `read_room` decrypts via
`room.messages()` — UTD events surface as `event_type =
"m.room.encrypted"` with `body = "[unable to decrypt]"`.
Cross-signing and automatic key backup are not enabled for the first
pass: static bearer-token bot accounts can't bootstrap cross-signing
without MSC3967.
## Hive Matrix Space

View file

@ -9,6 +9,13 @@
//! UIAA flow already minted the token + user/device, hive-c0re just
//! handed us the bearer in a file. matrix-sdk's `restore_session` with
//! a constructed `MatrixSession` skips the login flow entirely.
//!
//! E2EE is enabled via `with_encryption_settings(EncryptionSettings::default())`.
//! Crypto keys are persisted in the sqlite store under `state_dir`
//! (survives container restarts, lost on `--purge`). Cross-signing and
//! automatic key backup are deliberately left at their defaults (disabled)
//! for the first pass: bot accounts authenticated with a static bearer token
//! can't bootstrap cross-signing without MSC3967 on the server side.
use std::path::Path;
@ -16,6 +23,7 @@ use anyhow::{Context, Result, anyhow};
use matrix_sdk::{
Client, SessionMeta, SessionTokens,
authentication::matrix::MatrixSession,
encryption::EncryptionSettings,
ruma::{OwnedDeviceId, OwnedUserId},
};
use serde::Deserialize;
@ -108,6 +116,7 @@ pub async fn build_and_restore(
let client = Client::builder()
.homeserver_url(homeserver)
.sqlite_store(state_dir, None)
.with_encryption_settings(EncryptionSettings::default())
.build()
.await
.with_context(|| format!("build matrix client for {homeserver}"))?;

View file

@ -116,6 +116,24 @@ fn extract_body(event: &matrix_sdk::ruma::events::AnyTimelineEvent) -> String {
}
}
/// Same as [`extract_body`] but for the `AnySyncTimelineEvent` variant
/// returned by `TimelineEvent::raw()` (the sdk-level decrypted form from
/// `room.messages()`).
fn extract_body_sync(event: &matrix_sdk::ruma::events::AnySyncTimelineEvent) -> String {
use matrix_sdk::ruma::events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent};
match event {
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(ev)) => ev
.as_original()
.map_or_else(String::new, |orig| match &orig.content.msgtype {
MessageType::Text(t) => t.body.clone(),
MessageType::Notice(n) => n.body.clone(),
MessageType::Emote(e) => format!("* {}", e.body),
_ => String::new(),
}),
_ => String::new(),
}
}
pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse {
let room = match resolve_room(client, room_ref).await {
Ok(r) => r,
@ -408,32 +426,37 @@ pub async fn invite_user(client: &Client, room_ref: &str, user_id: &str) -> Daem
}
pub async fn read_room(client: &Client, room_ref: &str, limit: Option<usize>) -> DaemonResponse {
use matrix_sdk::ruma::api::Direction;
use matrix_sdk::ruma::api::client::message::get_message_events;
use matrix_sdk::room::MessagesOptions;
let room = match resolve_room(client, room_ref).await {
Ok(r) => r,
Err(e) => return e,
};
let limit = limit.unwrap_or(50).min(200);
let mut req =
get_message_events::v3::Request::new(room.room_id().to_owned(), Direction::Backward);
req.limit = matrix_sdk::ruma::UInt::try_from(limit as u64)
.unwrap_or(matrix_sdk::ruma::UInt::from(50u32));
let resp = match client.send(req).await {
Ok(r) => r,
Err(e) => return DaemonResponse::error(format!("get_message_events: {e}")),
let limit_val = u32::try_from(limit.unwrap_or(50).min(200)).unwrap_or(50);
let mut opts = MessagesOptions::backward();
opts.limit = matrix_sdk::ruma::UInt::from(limit_val);
// room.messages() transparently decrypts events in encrypted rooms.
// UTD (unable to decrypt) events surface via `ev.kind.is_utd()` and
// get a sentinel body so claude knows decryption failed rather than
// seeing the raw encrypted blob.
let msgs = match room.messages(opts).await {
Ok(m) => m,
Err(e) => return DaemonResponse::error(format!("messages: {e}")),
};
let events: Vec<TimelineEvent> = resp
let events: Vec<TimelineEvent> = msgs
.chunk
.iter()
.filter_map(|raw| {
let parsed = raw.deserialize().ok()?;
.filter_map(|ev| {
let parsed = ev.raw().deserialize().ok()?;
let event_id = parsed.event_id().to_string();
let sender = parsed.sender().to_string();
let origin_server_ts: i64 = parsed.origin_server_ts().0.into();
let event_type = parsed.event_type().to_string();
let body = extract_body(&parsed);
let body = if ev.kind.is_utd() {
"[unable to decrypt]".to_owned()
} else {
extract_body_sync(&parsed)
};
Some(TimelineEvent {
event_id,
sender,

View file

@ -241,6 +241,25 @@ in
'';
};
allowEncryption = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Server-side switch for matrix end-to-end encryption sets
tuwunel's `allow_encryption`. Off by default: on the hive-internal
homeserver the operator already controls the transport, so server
E2EE adds key-management overhead (cross-signing, device
verification, undecryptable-message recovery) without a clear
threat-model win for the common single-hive case. Turn on when
agents join encrypted rooms on external / federated homeservers,
or when the operator wants message contents opaque to the
homeserver admin. Independent of the agent matrix client, which
always supports decryption so it can read encrypted rooms it is
invited to regardless of this flag; this option only governs
whether THIS homeserver permits room encryption.
'';
};
gui = {
enable = lib.mkOption {
type = lib.types.bool;
@ -403,8 +422,9 @@ in
# LoadCredential below copies the host file into a
# 0400 dynamic-user-owned path; tuwunel reads from there.
registration_token_file = "/run/credentials/tuwunel.service/registration_token";
# E2EE disabled in initial rollout; tracked in the issue tracker.
allow_encryption = false;
# Server-side E2EE is opt-in (default off); the agent matrix
# client always supports decryption regardless.
allow_encryption = cfg.allowEncryption;
# Tuwunel's default suffix is " 💕" — suppress it so agent
# display names are clean (just the agent name, no emoji).
new_user_displayname_suffix = "";