···1+CREATE INDEX IF NOT EXISTS idx_session_tokens_did_created_at
2+ON session_tokens(did, created_at DESC);
3+4+CREATE INDEX IF NOT EXISTS idx_oauth_token_did_expires_at
5+ON oauth_token(did, expires_at DESC);
6+7+CREATE INDEX IF NOT EXISTS idx_oauth_token_did_created_at
8+ON oauth_token(did, created_at DESC);
9+10+CREATE INDEX IF NOT EXISTS idx_session_tokens_did_refresh_expires
11+ON session_tokens(did, refresh_expires_at DESC);
12+13+CREATE INDEX IF NOT EXISTS idx_app_passwords_user_created
14+ON app_passwords(user_id, created_at DESC);
15+16+CREATE INDEX IF NOT EXISTS idx_records_repo_collection_rkey
17+ON records(repo_id, collection, rkey);
18+19+CREATE INDEX IF NOT EXISTS idx_passkeys_did_created
20+ON passkeys(did, created_at DESC);
21+22+CREATE INDEX IF NOT EXISTS idx_backup_codes_did_unused
23+ON backup_codes(did) WHERE used_at IS NULL;
+125-31
src/api/admin/account/info.rs
···217 _auth: BearerAuthAdmin,
218 RawQuery(raw_query): RawQuery,
219) -> Response {
220- let dids = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids");
000221 if dids.is_empty() {
222 return (
223 StatusCode::BAD_REQUEST,
···225 )
226 .into_response();
227 }
228- let mut infos = Vec::new();
229- for did in &dids {
230- if did.is_empty() {
231- continue;
000000000000000232 }
233- let result = sqlx::query!(
00000000000000000000234 r#"
235- SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
236- FROM users
237- WHERE did = $1
0238 "#,
239- did
240 )
241- .fetch_optional(&state.db)
242- .await;
243- if let Ok(Some(row)) = result {
244- let invited_by = get_invited_by(&state.db, row.id).await;
245- let invites = get_invites_for_user(&state.db, row.id).await;
246- infos.push(AccountInfo {
247- did: row.did,
248- handle: row.handle,
249- email: row.email,
250- indexed_at: row.created_at.to_rfc3339(),
251- invite_note: None,
252- invites_disabled: row.invites_disabled.unwrap_or(false),
253- email_confirmed_at: if row.email_verified {
254- Some(row.created_at.to_rfc3339())
255- } else {
256- None
257- },
258- deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
259- invited_by,
260- invites,
00000000000261 });
262- }
00000000000000000000000000000000000000000000263 }
264 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
265}
···307 assert_eq!(res.status(), StatusCode::OK);
308 let body: serde_json::Value = res.json().await.unwrap();
309 let uri = body["uri"].as_str().unwrap();
310- let rkey = uri.split('/').last().unwrap().to_string();
311 rkeys.push(rkey);
312 }
313 for rkey in &rkeys {
···307 assert_eq!(res.status(), StatusCode::OK);
308 let body: serde_json::Value = res.json().await.unwrap();
309 let uri = body["uri"].as_str().unwrap();
310+ let rkey = uri.split('/').next_back().unwrap().to_string();
311 rkeys.push(rkey);
312 }
313 for rkey in &rkeys {
+4-4
tests/import_with_verification.rs
···192 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
193 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
194 let pds_endpoint = format!("https://{}", hostname);
195- let handle = did.split(':').last().unwrap_or("user");
196 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
197 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
198 unsafe {
···236 SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
237 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
238 let pds_endpoint = format!("https://{}", hostname);
239- let handle = did.split(':').last().unwrap_or("user");
240 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint);
241 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
242 unsafe {
···285 let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
286 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
287 let pds_endpoint = format!("https://{}", hostname);
288- let handle = did.split(':').last().unwrap_or("user");
289 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
290 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
291 unsafe {
···370 .await
371 .expect("Failed to get user signing key");
372 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
373- let handle = did.split(':').last().unwrap_or("user");
374 let did_doc_without_key = json!({
375 "@context": ["https://www.w3.org/ns/did/v1"],
376 "id": did,
···192 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
193 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
194 let pds_endpoint = format!("https://{}", hostname);
195+ let handle = did.split(':').next_back().unwrap_or("user");
196 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
197 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
198 unsafe {
···236 SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
237 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
238 let pds_endpoint = format!("https://{}", hostname);
239+ let handle = did.split(':').next_back().unwrap_or("user");
240 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint);
241 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
242 unsafe {
···285 let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
286 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
287 let pds_endpoint = format!("https://{}", hostname);
288+ let handle = did.split(':').next_back().unwrap_or("user");
289 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
290 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
291 unsafe {
···370 .await
371 .expect("Failed to get user signing key");
372 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
373+ let handle = did.split(':').next_back().unwrap_or("user");
374 let did_doc_without_key = json!({
375 "@context": ["https://www.w3.org/ns/did/v1"],
376 "id": did,
+6-6
tests/jwt_security.rs
···44 let token = create_access_token(did, &key_bytes).expect("create token");
45 let parts: Vec<&str> = token.split('.').collect();
4647- let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
48 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature);
49 let result = verify_access_token(&forged_token, &key_bytes);
50 assert!(result.is_err(), "Forged signature must be rejected");
···121 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap();
122 mac.update(message.as_bytes());
123 let hmac_sig = mac.finalize().into_bytes();
124- let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(&hmac_sig));
125 assert!(
126 verify_access_token(&hs256_token, &key_bytes).is_err(),
127 "HS256 substitution must be rejected"
···130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
133- let fake_sig = URL_SAFE_NO_PAD.encode(&vec![1u8; sig_len]);
134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
135 assert!(
136 verify_access_token(&token, &key_bytes).is_err(),
···335336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
338- let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
339 assert!(
340 verify_access_token(
341 &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig),
···439440 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#);
441 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#);
442- let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
443 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
444 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub");
445···479 "{}.{}.{}",
480 parts[0],
481 parts[1],
482- URL_SAFE_NO_PAD.encode(&[0xFFu8; 64])
483 );
484 let _ = verify_access_token(&almost_valid_token, &key_bytes);
485 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
···44 let token = create_access_token(did, &key_bytes).expect("create token");
45 let parts: Vec<&str> = token.split('.').collect();
4647+ let forged_signature = URL_SAFE_NO_PAD.encode([0u8; 64]);
48 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature);
49 let result = verify_access_token(&forged_token, &key_bytes);
50 assert!(result.is_err(), "Forged signature must be rejected");
···121 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap();
122 mac.update(message.as_bytes());
123 let hmac_sig = mac.finalize().into_bytes();
124+ let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(hmac_sig));
125 assert!(
126 verify_access_token(&hs256_token, &key_bytes).is_err(),
127 "HS256 substitution must be rejected"
···130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
133+ let fake_sig = URL_SAFE_NO_PAD.encode(vec![1u8; sig_len]);
134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
135 assert!(
136 verify_access_token(&token, &key_bytes).is_err(),
···335336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
338+ let fake_sig = URL_SAFE_NO_PAD.encode([1u8; 64]);
339 assert!(
340 verify_access_token(
341 &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig),
···439440 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#);
441 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#);
442+ let fake_sig = URL_SAFE_NO_PAD.encode([0u8; 64]);
443 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
444 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub");
445···479 "{}.{}.{}",
480 parts[0],
481 parts[1],
482+ URL_SAFE_NO_PAD.encode([0xFFu8; 64])
483 );
484 let _ = verify_access_token(&almost_valid_token, &key_bytes);
485 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
···461 let did = account["did"].as_str().unwrap().to_string();
462 let jwt = verify_new_account(&client, &did).await;
463 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
464- let post_rkey = post_uri.split('/').last().unwrap();
465 let status_before = client
466 .get(format!(
467 "{}/xrpc/com.atproto.server.checkAccountStatus",
···461 let did = account["did"].as_str().unwrap().to_string();
462 let jwt = verify_new_account(&client, &did).await;
463 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
464+ let post_rkey = post_uri.split('/').next_back().unwrap();
465 let status_before = client
466 .get(format!(
467 "{}/xrpc/com.atproto.server.checkAccountStatus",
+4-4
tests/lifecycle_social.rs
···14 let (post_uri, post_cid) =
15 create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
16 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
17- let like_rkey = like_uri.split('/').last().unwrap();
18 let get_like_res = client
19 .get(format!(
20 "{}/xrpc/com.atproto.repo.getRecord",
···74 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
75 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
76 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
77- let repost_rkey = repost_uri.split('/').last().unwrap();
78 let get_repost_res = client
79 .get(format!(
80 "{}/xrpc/com.atproto.repo.getRecord",
···119 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
120 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
121 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
122- let follow_rkey = follow_uri.split('/').last().unwrap();
123 let get_follow_res = client
124 .get(format!(
125 "{}/xrpc/com.atproto.repo.getRecord",
···240 .query(&[
241 ("repo", did.as_str()),
242 ("collection", "app.bsky.feed.post"),
243- ("rkey", post_uri.split('/').last().unwrap()),
244 ])
245 .send()
246 .await
···14 let (post_uri, post_cid) =
15 create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
16 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
17+ let like_rkey = like_uri.split('/').next_back().unwrap();
18 let get_like_res = client
19 .get(format!(
20 "{}/xrpc/com.atproto.repo.getRecord",
···74 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
75 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
76 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
77+ let repost_rkey = repost_uri.split('/').next_back().unwrap();
78 let get_repost_res = client
79 .get(format!(
80 "{}/xrpc/com.atproto.repo.getRecord",
···119 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
120 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
121 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
122+ let follow_rkey = follow_uri.split('/').next_back().unwrap();
123 let get_follow_res = client
124 .get(format!(
125 "{}/xrpc/com.atproto.repo.getRecord",
···240 .query(&[
241 ("repo", did.as_str()),
242 ("collection", "app.bsky.feed.post"),
243+ ("rkey", post_uri.split('/').next_back().unwrap()),
244 ])
245 .send()
246 .await
+3-3
tests/oauth.rs
···21 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
22 let mut hasher = Sha256::new();
23 hasher.update(code_verifier.as_bytes());
24- let code_challenge = URL_SAFE_NO_PAD.encode(&hasher.finalize());
25 (code_verifier, code_challenge)
26}
27···1036 );
1037 let body: Value = create_res.json().await.unwrap();
1038 let uri = body["uri"].as_str().expect("Should have uri");
1039- let rkey = uri.split('/').last().unwrap();
1040 let delete_res = http_client
1041 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
1042 .bearer_auth(&token)
···1092 );
1093 let body: Value = post_res.json().await.unwrap();
1094 let uri = body["uri"].as_str().unwrap();
1095- let rkey = uri.split('/').last().unwrap();
1096 let delete_res = http_client
1097 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
1098 .bearer_auth(&token)
···21 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
22 let mut hasher = Sha256::new();
23 hasher.update(code_verifier.as_bytes());
24+ let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
25 (code_verifier, code_challenge)
26}
27···1036 );
1037 let body: Value = create_res.json().await.unwrap();
1038 let uri = body["uri"].as_str().expect("Should have uri");
1039+ let rkey = uri.split('/').next_back().unwrap();
1040 let delete_res = http_client
1041 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
1042 .bearer_auth(&token)
···1092 );
1093 let body: Value = post_res.json().await.unwrap();
1094 let uri = body["uri"].as_str().unwrap();
1095+ let rkey = uri.split('/').next_back().unwrap();
1096 let delete_res = http_client
1097 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
1098 .bearer_auth(&token)
+3-3
tests/oauth_lifecycle.rs
···17 let mut hasher = Sha256::new();
18 hasher.update(code_verifier.as_bytes());
19 let hash = hasher.finalize();
20- let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
21 (code_verifier, code_challenge)
22}
23···195 );
196 let create_body: Value = create_res.json().await.unwrap();
197 let uri = create_body["uri"].as_str().unwrap();
198- let rkey = uri.split('/').last().unwrap();
199 let get_res = http_client
200 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
201 .bearer_auth(&session.access_token)
···290 assert_eq!(create_res.status(), StatusCode::OK);
291 let create_body: Value = create_res.json().await.unwrap();
292 let uri = create_body["uri"].as_str().unwrap();
293- let rkey = uri.split('/').last().unwrap();
294 let updated_text = "Updated post content via OAuth putRecord";
295 let put_res = http_client
296 .post(format!("{}/xrpc/com.atproto.repo.putRecord", url))
···17 let mut hasher = Sha256::new();
18 hasher.update(code_verifier.as_bytes());
19 let hash = hasher.finalize();
20+ let code_challenge = URL_SAFE_NO_PAD.encode(hash);
21 (code_verifier, code_challenge)
22}
23···195 );
196 let create_body: Value = create_res.json().await.unwrap();
197 let uri = create_body["uri"].as_str().unwrap();
198+ let rkey = uri.split('/').next_back().unwrap();
199 let get_res = http_client
200 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
201 .bearer_auth(&session.access_token)
···290 assert_eq!(create_res.status(), StatusCode::OK);
291 let create_body: Value = create_res.json().await.unwrap();
292 let uri = create_body["uri"].as_str().unwrap();
293+ let rkey = uri.split('/').next_back().unwrap();
294 let updated_text = "Updated post content via OAuth putRecord";
295 let put_res = http_client
296 .post(format!("{}/xrpc/com.atproto.repo.putRecord", url))
+2-2
tests/oauth_scopes.rs
···17 let mut hasher = Sha256::new();
18 hasher.update(code_verifier.as_bytes());
19 let hash = hasher.finalize();
20- let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
21 (code_verifier, code_challenge)
22}
23···215 .as_str()
216 .unwrap()
217 .split('/')
218- .last()
219 .unwrap();
220221 let put_res = http_client
···17 let mut hasher = Sha256::new();
18 hasher.update(code_verifier.as_bytes());
19 let hash = hasher.finalize();
20+ let code_challenge = URL_SAFE_NO_PAD.encode(hash);
21 (code_verifier, code_challenge)
22}
23···215 .as_str()
216 .unwrap()
217 .split('/')
218+ .next_back()
219 .unwrap();
220221 let put_res = http_client
···727 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
728 base_url().await,
729 did,
730- original_uri.split('/').last().unwrap()
731 ))
732 .send()
733 .await
···970 .as_array()
971 .expect("Should have records array");
972 assert!(
973- records.len() >= 1,
974 "Should have at least 1 record after migration, found {}",
975 records.len()
976 );
···727 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
728 base_url().await,
729 did,
730+ original_uri.split('/').next_back().unwrap()
731 ))
732 .send()
733 .await
···970 .as_array()
971 .expect("Should have records array");
972 assert!(
973+ !records.is_empty(),
974 "Should have at least 1 record after migration, found {}",
975 records.len()
976 );
+1-1
tests/plc_operations.rs
···114 .await
115 .unwrap();
116 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
117- let handle = did.split(':').last().unwrap_or("user");
118 let res = client.post(format!("{}/xrpc/com.atproto.identity.submitPlcOperation", base_url().await))
119 .bearer_auth(&token).json(&json!({
120 "operation": { "type": "plc_operation", "rotationKeys": ["did:key:z123"],
···114 .await
115 .unwrap();
116 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
117+ let handle = did.split(':').next_back().unwrap_or("user");
118 let res = client.post(format!("{}/xrpc/com.atproto.identity.submitPlcOperation", base_url().await))
119 .bearer_auth(&token).json(&json!({
120 "operation": { "type": "plc_operation", "rotationKeys": ["did:key:z123"],
+1-1
tests/plc_validation.rs
···172 "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "prev": null
173 });
174 let signed = sign_operation(&op, &key).unwrap();
175- let result = verify_operation_signature(&signed, &[did_key.clone()]);
176 assert!(result.is_ok() && result.unwrap());
177178 let other_key = SigningKey::random(&mut rand::thread_rng());
···172 "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "prev": null
173 });
174 let signed = sign_operation(&op, &key).unwrap();
175+ let result = verify_operation_signature(&signed, std::slice::from_ref(&did_key));
176 assert!(result.is_ok() && result.unwrap());
177178 let other_key = SigningKey::random(&mut rand::thread_rng());