Alias TTL renews only on authenticated actions (sending messages)
- Sending a message includes `from` fingerprint - Server renews alias TTL on send (proves identity: you encrypted it) - Polling/receiving does NOT renew (anyone can spam messages to you) - Key registration does NOT renew (separate concern) This prevents alias keepalive attacks where someone spams a user just to keep their alias from expiring. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@ pub async fn run(recipient_fp: &str, message: &str, server_url: &str) -> Result<
|
|||||||
let encoded = bincode::serialize(&wire_msg)
|
let encoded = bincode::serialize(&wire_msg)
|
||||||
.context("failed to serialize wire message")?;
|
.context("failed to serialize wire message")?;
|
||||||
|
|
||||||
client.send_message(recipient_fp, &encoded).await?;
|
client.send_message(recipient_fp, Some(&our_pub.fingerprint.to_string()), &encoded).await?;
|
||||||
|
|
||||||
println!("Message sent to {}", recipient_fp);
|
println!("Message sent to {}", recipient_fp);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct RegisterRequest {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct SendRequest {
|
struct SendRequest {
|
||||||
to: String,
|
to: String,
|
||||||
|
from: Option<String>,
|
||||||
message: Vec<u8>,
|
message: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +94,13 @@ impl ServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send an encrypted message to the server for delivery.
|
/// Send an encrypted message to the server for delivery.
|
||||||
pub async fn send_message(&self, to: &str, message: &[u8]) -> Result<()> {
|
pub async fn send_message(&self, to: &str, from: Option<&str>, message: &[u8]) -> Result<()> {
|
||||||
let to_clean: String = to.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
let to_clean: String = to.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||||
self.client
|
self.client
|
||||||
.post(format!("{}/v1/messages/send", self.base_url))
|
.post(format!("{}/v1/messages/send", self.base_url))
|
||||||
.json(&SendRequest {
|
.json(&SendRequest {
|
||||||
to: to_clean,
|
to: to_clean,
|
||||||
|
from: from.map(|f| f.chars().filter(|c| c.is_ascii_hexdigit()).collect()),
|
||||||
message: message.to_vec(),
|
message: message.to_vec(),
|
||||||
})
|
})
|
||||||
.send()
|
.send()
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.send_message(&peer, &encoded).await {
|
match client.send_message(&peer, Some(&self.our_fp), &encoded).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.add_message(ChatLine {
|
self.add_message(ChatLine {
|
||||||
sender: self.our_fp[..12].to_string(),
|
sender: self.our_fp[..12].to_string(),
|
||||||
|
|||||||
@@ -8,6 +8,25 @@ use serde::Deserialize;
|
|||||||
use crate::errors::AppResult;
|
use crate::errors::AppResult;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
/// Touch the alias TTL for a fingerprint (renew on authenticated action).
|
||||||
|
fn renew_alias_ttl(db: &sled::Tree, fp: &str) {
|
||||||
|
let alias_key = format!("fp:{}", fp);
|
||||||
|
if let Ok(Some(alias_bytes)) = db.get(alias_key.as_bytes()) {
|
||||||
|
let alias = String::from_utf8_lossy(&alias_bytes).to_string();
|
||||||
|
let rec_key = format!("rec:{}", alias);
|
||||||
|
if let Ok(Some(rec_data)) = db.get(rec_key.as_bytes()) {
|
||||||
|
if let Ok(mut record) = serde_json::from_slice::<serde_json::Value>(&rec_data) {
|
||||||
|
if let Some(obj) = record.as_object_mut() {
|
||||||
|
obj.insert("last_active".into(), serde_json::json!(chrono::Utc::now().timestamp()));
|
||||||
|
if let Ok(updated) = serde_json::to_vec(&record) {
|
||||||
|
let _ = db.insert(rec_key.as_bytes(), updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/messages/send", post(send_message))
|
.route("/messages/send", post(send_message))
|
||||||
@@ -18,6 +37,8 @@ pub fn routes() -> Router<AppState> {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SendRequest {
|
struct SendRequest {
|
||||||
to: String,
|
to: String,
|
||||||
|
#[serde(default)]
|
||||||
|
from: Option<String>,
|
||||||
message: Vec<u8>,
|
message: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +57,12 @@ async fn send_message(
|
|||||||
let key = format!("queue:{}:{}", to, uuid::Uuid::new_v4());
|
let key = format!("queue:{}:{}", to, uuid::Uuid::new_v4());
|
||||||
tracing::info!("Queuing message for {} ({} bytes)", to, req.message.len());
|
tracing::info!("Queuing message for {} ({} bytes)", to, req.message.len());
|
||||||
state.db.messages.insert(key.as_bytes(), req.message)?;
|
state.db.messages.insert(key.as_bytes(), req.message)?;
|
||||||
|
|
||||||
|
// Renew sender's alias TTL (sending = authenticated action)
|
||||||
|
if let Some(ref from) = req.from {
|
||||||
|
renew_alias_ttl(&state.db.aliases, &normalize_fp(from));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({ "ok": true })))
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user