diff --git a/android/ffi/src/lib.rs b/android/ffi/src/lib.rs
index 775f144..9576f6f 100644
--- a/android/ffi/src/lib.rs
+++ b/android/ffi/src/lib.rs
@@ -753,14 +753,8 @@ impl Inkwell {
/// it (`state::open_token`), so the app asks to sign in again.
fn credentials(&self) -> Result<(String, String), CoreError> {
let conn = self.db.conn().map_err(CoreError::store)?;
- let current = state::read(&conn).map_err(CoreError::store)?;
- let (Some(url), Some(stored)) = (current.server_url, current.device_token) else {
- return Err(CoreError::NotLinked);
- };
- match state::open_token(&conn, &stored, &self.seal).map_err(CoreError::store)? {
- Some(token) => Ok((url, token)),
- None => Err(CoreError::NotLinked),
- }
+ let link = state::credentials(&conn, Some(&self.seal)).map_err(CoreError::store)?;
+ link.ok_or(CoreError::NotLinked)
}
/// Persist a fresh link, adopting the server's retention window at the same time
diff --git a/core/src/sync/sharing.rs b/core/src/sync/sharing.rs
index 38df0f9..3ab1acd 100644
--- a/core/src/sync/sharing.rs
+++ b/core/src/sync/sharing.rs
@@ -6,10 +6,10 @@
//! 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).
+//! The server address and token come from the CALLER, read with
+//! `state::credentials` and the caller's seal. A stored token may be sealed
+//! (Android keeps it under a Keystore key), and reading it raw here sent `sealed:…`
+//! as the bearer token, so every share call was refused (#5381).
use rusqlite::params;
@@ -22,17 +22,12 @@ 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.";
-/// 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.
+/// The link to share over, or [`NEEDS_SERVER`]. For a client that keeps its token
+/// plain, as the desktop does; one with a seal reads `state::credentials` itself.
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) {
- (Some(url), Some(token)) => Ok((url, token)),
- _ => Err(NEEDS_SERVER.to_string()),
- }
+ let link = state::credentials(&conn, None).map_err(|e| e.to_string())?;
+ link.ok_or_else(|| NEEDS_SERVER.to_string())
}
/// Set the local note's `shared` flag from the server's list of its shares. Not a
diff --git a/core/src/sync/state.rs b/core/src/sync/state.rs
index 05e7bc0..5db0a71 100644
--- a/core/src/sync/state.rs
+++ b/core/src/sync/state.rs
@@ -175,6 +175,27 @@ pub fn open_token(
Ok(Some(stored.to_string()))
}
+/// The server address and the token to send it, or None when this device isn't
+/// linked. The ONE read of a link for a call to the server: every client goes
+/// through here, so none can send a token as stored when it is sealed (#5381).
+///
+/// With a `seal`, the stored token is opened ([`open_token`]); a client without one
+/// (the desktop keeps its token plain) passes None and gets it as stored.
+pub fn credentials(
+ conn: &Connection,
+ seal: Option<&dyn TokenSeal>,
+) -> rusqlite::Result