Android sharing sends the opened device token, not the sealed one
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
Android / Core and FFI clippy and tests (push) Successful in 34s
CI & Build / integration (push) Successful in 1m39s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m58s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m11s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m17s
Android / Build the server image (push) Successful in 1s

Android has stored its device token sealed ("sealed:…") since 8592b83, and
core's sharing calls read the token from the store themselves. The ffi opened
it in credentials() and then threw the result away, so every Share-sheet
request went out as `Bearer sealed:…` and the server refused it.

The sharing functions now take the server address and token from the caller.
The ffi passes what credentials() opened; the desktop, which stores its token
plain, reads it through sharing::stored_link. A new ffi test serves one request
on a loopback port and checks the bearer token that arrives (#5381).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:05:10 -04:00
co-authored by Claude Opus 5.5
parent 39b1ebae96
commit be4897276c
3 changed files with 128 additions and 27 deletions
+31 -13
View File
@@ -5,6 +5,11 @@
//! device token. The one thing kept locally is the note's `shared` flag, set from
//! the server's answer so the card's chip changes at once rather than at the next
//! sync (which brings the same value).
//!
//! The server address and token come from the CALLER, never from the store here.
//! A stored token may be sealed (Android keeps it under a Keystore key, see
//! `state::TokenSeal`), and only the caller holds the seal that opens it. Reading it
//! here sent `sealed:…` as the bearer token, and every share call was refused (#5381).
use rusqlite::params;
@@ -17,7 +22,11 @@ use crate::local::Db;
pub const NEEDS_SERVER: &str =
"Sharing is between people on a server. Link this device to one in Sync to share notes.";
fn link(db: &Db) -> Result<(String, String), String> {
/// The server address and token AS STORED, or [`NEEDS_SERVER`].
///
/// Only for a client that stores its token plain, as the desktop does. A client
/// with a seal opens the token itself (`state::open_token`) and passes that.
pub fn stored_link(db: &Db) -> Result<(String, String), String> {
let conn = db.conn()?;
let link = state::read(&conn).map_err(|e| e.to_string())?;
match (link.server_url, link.device_token) {
@@ -38,33 +47,42 @@ fn mark_shared(db: &Db, note_id: &str, shares: &[NoteShare]) -> Result<(), Strin
Ok(())
}
pub async fn directory(db: &Db) -> Result<Directory, String> {
let (url, token) = link(db)?;
client::directory(&url, &token).await
pub async fn directory(url: &str, token: &str) -> Result<Directory, String> {
client::directory(url, token).await
}
pub async fn list(db: &Db, note_id: &str) -> Result<Vec<NoteShare>, String> {
let (url, token) = link(db)?;
let shares = client::list_shares(&url, &token, note_id).await?;
pub async fn list(
db: &Db,
url: &str,
token: &str,
note_id: &str,
) -> Result<Vec<NoteShare>, String> {
let shares = client::list_shares(url, token, note_id).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
pub async fn share(
db: &Db,
url: &str,
token: &str,
note_id: &str,
target: &ShareTarget,
permission: &str,
) -> Result<Vec<NoteShare>, String> {
let (url, token) = link(db)?;
let shares = client::share_note(&url, &token, note_id, target, permission).await?;
let shares = client::share_note(url, token, note_id, target, permission).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
pub async fn unshare(db: &Db, note_id: &str, share_id: &str) -> Result<Vec<NoteShare>, String> {
let (url, token) = link(db)?;
let shares = client::unshare_note(&url, &token, note_id, share_id).await?;
pub async fn unshare(
db: &Db,
url: &str,
token: &str,
note_id: &str,
share_id: &str,
) -> Result<Vec<NoteShare>, String> {
let shares = client::unshare_note(url, token, note_id, share_id).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
@@ -85,7 +103,7 @@ mod tests {
#[test]
fn an_unlinked_device_explains_that_sharing_needs_a_server() {
assert_eq!(link(&db()).unwrap_err(), NEEDS_SERVER);
assert_eq!(stored_link(&db()).unwrap_err(), NEEDS_SERVER);
}
#[test]