Skip to main content

futu_backend/auth/device/
websig_store.rs

1use super::CredentialsStoreError;
2use super::credentials_load::load_credentials_from_path_unlocked;
3use super::credentials_lock::with_credentials_exclusive_path;
4use super::credentials_store::save_credentials_to_path_unlocked;
5use super::storage::try_credentials_path;
6
7/// Fail-closed errors returned while updating an existing credential record.
8#[derive(Debug)]
9pub enum WebSigPersistenceError {
10    MissingCredentials,
11    UserIdMismatch,
12    EmptyTicket,
13    StoreLockPoisoned,
14    Store(String),
15}
16
17impl std::fmt::Display for WebSigPersistenceError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Self::MissingCredentials => write!(f, "complete credentials are not available"),
21            Self::UserIdMismatch => write!(f, "credential user identity changed"),
22            Self::EmptyTicket => write!(f, "refreshed WebSig ticket is empty"),
23            Self::StoreLockPoisoned => write!(f, "credential WebSig store lock is poisoned"),
24            Self::Store(message) => write!(f, "credential WebSig persistence failed: {message}"),
25        }
26    }
27}
28
29impl std::error::Error for WebSigPersistenceError {}
30
31impl From<CredentialsStoreError> for WebSigPersistenceError {
32    fn from(value: CredentialsStoreError) -> Self {
33        Self::Store(value.to_string())
34    }
35}
36
37fn with_current_credentials(
38    account: &str,
39    user_id: u64,
40    update: impl FnOnce(&mut super::SavedCredentials),
41) -> Result<(), WebSigPersistenceError> {
42    let path =
43        try_credentials_path(account).map_err(|_| WebSigPersistenceError::MissingCredentials)?;
44    with_credentials_exclusive_path(&path, || {
45        let mut credentials = load_credentials_from_path_unlocked(account, &path)
46            .ok_or(WebSigPersistenceError::MissingCredentials)?;
47        if credentials.uid != user_id {
48            return Err(WebSigPersistenceError::UserIdMismatch);
49        }
50        update(&mut credentials);
51        save_credentials_to_path_unlocked(&path, &credentials)?;
52        Ok(())
53    })
54    .map_err(|error| WebSigPersistenceError::Store(error.to_string()))?
55}
56
57/// Persist the Platform tickets returned by `CMD1013` without replacing the
58/// rest of the account credential record.
59pub fn persist_platform_websigs(
60    account: &str,
61    user_id: u64,
62    web_sig: &str,
63    moomoo_web_sig: &str,
64) -> Result<(), WebSigPersistenceError> {
65    if web_sig.is_empty() || moomoo_web_sig.is_empty() {
66        return Err(WebSigPersistenceError::EmptyTicket);
67    }
68    with_current_credentials(account, user_id, |credentials| {
69        credentials.web_sig = web_sig.to_string();
70        credentials.moomoo_web_sig = moomoo_web_sig.to_string();
71    })
72}
73
74/// Persist one Broker ticket returned by `CMD20203` while preserving tickets
75/// owned by other broker channels.
76pub fn persist_broker_websig(
77    account: &str,
78    user_id: u64,
79    broker_id: u32,
80    broker_web_sig: &str,
81) -> Result<(), WebSigPersistenceError> {
82    if broker_web_sig.is_empty() {
83        return Err(WebSigPersistenceError::EmptyTicket);
84    }
85    with_current_credentials(account, user_id, |credentials| {
86        credentials
87            .broker_web_sigs
88            .insert(broker_id, broker_web_sig.to_string());
89    })
90}