One read of a device's link: state::credentials, with the client's seal
Five places read the server address and token straight from sync_state: sharing, autosync, sync_unlink, update's download token, and the ffi. Reading it raw is how Android came to send its sealed token to the share routes (#5381). state::credentials(conn, seal) now holds that read. With a seal it opens the token (open_token), and without one (the desktop keeps it plain) it returns it as stored. Every site calls it. DRY pass #2, batch 1, F1 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -753,14 +753,8 @@ impl Inkwell {
|
|||||||
/// it (`state::open_token`), so the app asks to sign in again.
|
/// it (`state::open_token`), so the app asks to sign in again.
|
||||||
fn credentials(&self) -> Result<(String, String), CoreError> {
|
fn credentials(&self) -> Result<(String, String), CoreError> {
|
||||||
let conn = self.db.conn().map_err(CoreError::store)?;
|
let conn = self.db.conn().map_err(CoreError::store)?;
|
||||||
let current = state::read(&conn).map_err(CoreError::store)?;
|
let link = state::credentials(&conn, Some(&self.seal)).map_err(CoreError::store)?;
|
||||||
let (Some(url), Some(stored)) = (current.server_url, current.device_token) else {
|
link.ok_or(CoreError::NotLinked)
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist a fresh link, adopting the server's retention window at the same time
|
/// Persist a fresh link, adopting the server's retention window at the same time
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
//! the server's answer so the card's chip changes at once rather than at the next
|
//! the server's answer so the card's chip changes at once rather than at the next
|
||||||
//! sync (which brings the same value).
|
//! sync (which brings the same value).
|
||||||
//!
|
//!
|
||||||
//! The server address and token come from the CALLER, never from the store here.
|
//! The server address and token come from the CALLER, read with
|
||||||
//! A stored token may be sealed (Android keeps it under a Keystore key, see
|
//! `state::credentials` and the caller's seal. A stored token may be sealed
|
||||||
//! `state::TokenSeal`), and only the caller holds the seal that opens it. Reading it
|
//! (Android keeps it under a Keystore key), and reading it raw here sent `sealed:…`
|
||||||
//! here sent `sealed:…` as the bearer token, and every share call was refused (#5381).
|
//! as the bearer token, so every share call was refused (#5381).
|
||||||
|
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
|
|
||||||
@@ -22,17 +22,12 @@ use crate::local::Db;
|
|||||||
pub const NEEDS_SERVER: &str =
|
pub const NEEDS_SERVER: &str =
|
||||||
"Sharing is between people on a server. Link this device to one in Sync to share notes.";
|
"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`].
|
/// 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.
|
||||||
/// 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> {
|
pub fn stored_link(db: &Db) -> Result<(String, String), String> {
|
||||||
let conn = db.conn()?;
|
let conn = db.conn()?;
|
||||||
let link = state::read(&conn).map_err(|e| e.to_string())?;
|
let link = state::credentials(&conn, None).map_err(|e| e.to_string())?;
|
||||||
match (link.server_url, link.device_token) {
|
link.ok_or_else(|| NEEDS_SERVER.to_string())
|
||||||
(Some(url), Some(token)) => Ok((url, token)),
|
|
||||||
_ => Err(NEEDS_SERVER.to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the local note's `shared` flag from the server's list of its shares. Not a
|
/// Set the local note's `shared` flag from the server's list of its shares. Not a
|
||||||
|
|||||||
@@ -175,6 +175,27 @@ pub fn open_token(
|
|||||||
Ok(Some(stored.to_string()))
|
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<Option<(String, String)>> {
|
||||||
|
let link = read(conn)?;
|
||||||
|
let (Some(url), Some(stored)) = (link.server_url, link.device_token) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let token = match seal {
|
||||||
|
Some(seal) => open_token(conn, &stored, seal)?,
|
||||||
|
None => Some(stored),
|
||||||
|
};
|
||||||
|
Ok(token.map(|token| (url, token)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the stored token alone, leaving the server and cursor as they are.
|
/// Replace the stored token alone, leaving the server and cursor as they are.
|
||||||
fn store_token(conn: &Connection, token: Option<&str>) -> rusqlite::Result<()> {
|
fn store_token(conn: &Connection, token: Option<&str>) -> rusqlite::Result<()> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
@@ -176,8 +176,7 @@ fn backoff(failures: u32) -> Duration {
|
|||||||
/// The server URL and token, or None when this device isn't linked.
|
/// The server URL and token, or None when this device isn't linked.
|
||||||
fn credentials(db: &Db) -> Result<Option<(String, String)>, String> {
|
fn credentials(db: &Db) -> Result<Option<(String, String)>, String> {
|
||||||
let conn = db.conn()?;
|
let conn = db.conn()?;
|
||||||
let current = state::read(&conn).map_err(|e| e.to_string())?;
|
state::credentials(&conn, None).map_err(|e| e.to_string())
|
||||||
Ok(current.server_url.zip(current.device_token))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What is waiting to be sent, as [`push::pending_fingerprint`] sees it. A store
|
/// What is waiting to be sent, as [`push::pending_fingerprint`] sees it. A store
|
||||||
|
|||||||
@@ -136,8 +136,7 @@ pub async fn sync_unlink(db: State<'_, Db>) -> Result<UnlinkResult, String> {
|
|||||||
// the UI.
|
// the UI.
|
||||||
let link = {
|
let link = {
|
||||||
let conn = db.conn()?;
|
let conn = db.conn()?;
|
||||||
let current = state::read(&conn).map_err(|e| e.to_string())?;
|
state::credentials(&conn, None).map_err(|e| e.to_string())?
|
||||||
current.server_url.zip(current.device_token)
|
|
||||||
};
|
};
|
||||||
let revoked = match &link {
|
let revoked = match &link {
|
||||||
Some((base_url, token)) => client::revoke_self(base_url, token).await,
|
Some((base_url, token)) => client::revoke_self(base_url, token).await,
|
||||||
|
|||||||
@@ -343,13 +343,10 @@ fn read_source(db: &Db) -> Result<Source, String> {
|
|||||||
/// the link is the one credential this app already holds for it.
|
/// the link is the one credential this app already holds for it.
|
||||||
fn token_for(db: &Db, base: &str) -> Result<Option<String>, String> {
|
fn token_for(db: &Db, base: &str) -> Result<Option<String>, String> {
|
||||||
let conn = db.conn()?;
|
let conn = db.conn()?;
|
||||||
let link = state::read(&conn).map_err(|e| e.to_string())?;
|
let Some((url, token)) = state::credentials(&conn, None).map_err(|e| e.to_string())? else {
|
||||||
let linked = link.server_url.as_deref().and_then(normalize_server);
|
return Ok(None);
|
||||||
if linked.as_deref() == Some(base) {
|
};
|
||||||
Ok(link.device_token)
|
Ok((normalize_server(&url).as_deref() == Some(base)).then_some(token))
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An updater pointed at `source`, carrying `token` when there is one. The plugin
|
/// An updater pointed at `source`, carrying `token` when there is one. The plugin
|
||||||
|
|||||||
Reference in New Issue
Block a user