diff --git a/TODO.md b/TODO.md index f1d7d3f..9cfb3dc 100644 --- a/TODO.md +++ b/TODO.md @@ -53,19 +53,6 @@ Modern passwordless authentication using WebAuthn/FIDO2, plus TOTP for defense i - [ ] Rate limit 2FA attempts - [ ] Re-auth for sensitive actions (email change, adding new auth methods) -### Private/encrypted data -Records that only authorized parties can see and decrypt. Requires key federation between PDSes. - -- [ ] Survey current ATProto discourse on private data -- [ ] Document Bluesky team's likely approach -- [ ] Design key management strategy -- [ ] Per-user encryption keys (separate from signing keys) -- [ ] Key derivation for per-record or per-collection encryption -- [ ] Encrypted record storage format -- [ ] Transparent encryption/decryption in repo operations -- [ ] Protocol for sharing decryption keys between PDSes -- [ ] Handle key rotation and revocation - ### Plugin system Extensible architecture allowing third-party plugins to add functionality, like minecraft mods or browser extensions. @@ -82,6 +69,19 @@ Extensible architecture allowing third-party plugins to add functionality, like - [ ] Example plugins: custom feed algorithm, content filter, S3 backup - [ ] Plugin registry with signature verification and version compatibility +### Plugin: Private/encrypted data +Records that only authorized parties can see and decrypt. Requires key federation between PDSes. Implemented as a plugin using the plugin system above. + +- [ ] Survey current ATProto discourse on private data +- [ ] Document Bluesky team's likely approach +- [ ] Design key management strategy +- [ ] Per-user encryption keys (separate from signing keys) +- [ ] Key derivation for per-record or per-collection encryption +- [ ] Encrypted record storage format +- [ ] Transparent encryption/decryption in repo operations +- [ ] Protocol for sharing decryption keys between PDSes +- [ ] Handle key rotation and revocation + --- ## Completed diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index db861b5..badd44b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -21,7 +21,6 @@ services: JWT_SECRET: "${JWT_SECRET:?JWT_SECRET is required (min 32 chars)}" DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" - APPVIEW_URL: "${APPVIEW_URL:-https://api.bsky.app}" CRAWLERS: "${CRAWLERS:-https://bsky.network}" FRONTEND_DIR: "/app/frontend/dist" depends_on: diff --git a/docs/install-alpine.md b/docs/install-alpine.md index 3209010..4cbbcc9 100644 --- a/docs/install-alpine.md +++ b/docs/install-alpine.md @@ -141,7 +141,7 @@ start_pre() { . /etc/bspds/bspds.env export SERVER_HOST SERVER_PORT PDS_HOSTNAME DATABASE_URL export S3_ENDPOINT AWS_REGION S3_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY - export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY APPVIEW_URL CRAWLERS + export VALKEY_URL JWT_SECRET DPOP_SECRET MASTER_KEY CRAWLERS } EOF chmod +x /etc/init.d/bspds diff --git a/docs/install-kubernetes.md b/docs/install-kubernetes.md index 8c00bd1..da188fa 100644 --- a/docs/install-kubernetes.md +++ b/docs/install-kubernetes.md @@ -15,7 +15,6 @@ The container image expects: - `VALKEY_URL` - redis:// connection string - `PDS_HOSTNAME` - your PDS hostname (without protocol) - `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48` -- `APPVIEW_URL` - typically `https://api.bsky.app` - `CRAWLERS` - typically `https://bsky.network` and more, check the .env.example. diff --git a/scripts/install-debian.sh b/scripts/install-debian.sh index b6fd963..3f0a6ff 100755 --- a/scripts/install-debian.sh +++ b/scripts/install-debian.sh @@ -389,7 +389,6 @@ JWT_SECRET=${JWT_SECRET} DPOP_SECRET=${DPOP_SECRET} MASTER_KEY=${MASTER_KEY} PLC_DIRECTORY_URL=https://plc.directory -APPVIEW_URL=https://api.bsky.app CRAWLERS=https://bsky.network AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN} diff --git a/src/api/repo/record/read.rs b/src/api/repo/record/read.rs index a8ab69c..ac9b62a 100644 --- a/src/api/repo/record/read.rs +++ b/src/api/repo/record/read.rs @@ -1,8 +1,9 @@ +use crate::api::proxy_client::proxy_client; use crate::state::AppState; use axum::{ Json, extract::{Query, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, }; use cid::Cid; @@ -11,7 +12,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; use std::str::FromStr; -use tracing::error; +use tracing::{error, info}; #[derive(Deserialize)] pub struct GetRecordInput { @@ -23,6 +24,7 @@ pub struct GetRecordInput { pub async fn get_record( State(state): State, + headers: HeaderMap, Query(input): Query, ) -> Response { let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); @@ -46,6 +48,63 @@ pub async fn get_record( let user_id: uuid::Uuid = match user_id_opt { Ok(Some(id)) => id, Ok(None) => { + if let Some(proxy_header) = headers + .get("atproto-proxy") + .and_then(|h| h.to_str().ok()) + { + let did = proxy_header.split('#').next().unwrap_or(proxy_header); + if let Some(resolved) = state.did_resolver.resolve_did(did).await { + let mut url = format!( + "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}", + resolved.url.trim_end_matches('/'), + urlencoding::encode(&input.repo), + urlencoding::encode(&input.collection), + urlencoding::encode(&input.rkey) + ); + if let Some(cid) = &input.cid { + url.push_str(&format!("&cid={}", urlencoding::encode(cid))); + } + info!("Proxying getRecord to {}: {}", did, url); + match proxy_client().get(&url).send().await { + Ok(resp) => { + let status = resp.status(); + let body = match resp.bytes().await { + Ok(b) => b, + Err(e) => { + error!("Error reading proxy response: {:?}", e); + return ( + StatusCode::BAD_GATEWAY, + Json(json!({"error": "UpstreamFailure", "message": "Error reading upstream response"})), + ) + .into_response(); + } + }; + return Response::builder() + .status(status) + .header("content-type", "application/json") + .body(axum::body::Body::from(body)) + .unwrap_or_else(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() + }); + } + Err(e) => { + error!("Error proxying request: {:?}", e); + return ( + StatusCode::BAD_GATEWAY, + Json(json!({"error": "UpstreamFailure", "message": "Failed to reach upstream service"})), + ) + .into_response(); + } + } + } else { + error!("Could not resolve DID from atproto-proxy header: {}", did); + return ( + StatusCode::BAD_GATEWAY, + Json(json!({"error": "UpstreamFailure", "message": "Could not resolve proxy DID"})), + ) + .into_response(); + } + } return ( StatusCode::NOT_FOUND, Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),