Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/unifiedpush.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ A `2xx` response is treated as success. Any other response is logged at `error!`

The endpoint store is loaded once at startup. Failures to read or parse the file are logged and the service starts with an empty map.

Logs never include raw UnifiedPush `device_id` values or endpoint URL prefixes.
Device IDs are represented by the same salted, truncated BLAKE3 correlator used
for other privacy-sensitive identifiers, with a random in-memory salt per
process.

## Platform support

`UnifiedPushService::supports_platform` returns `true` only for `Platform::Android`. iOS clients are FCM-only.
Expand Down
50 changes: 43 additions & 7 deletions src/push/unifiedpush.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use log::{debug, error, info, warn};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
Expand All @@ -10,6 +11,7 @@ use tokio::sync::RwLock;
use super::PushService;
use crate::config::Config;
use crate::store::Platform;
use crate::utils::log_pubkey::log_pubkey;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedPushEndpoint {
Expand All @@ -26,20 +28,28 @@ pub struct UnifiedPushService {
client: Arc<reqwest::Client>,
endpoints: RwLock<HashMap<String, UnifiedPushEndpoint>>,
storage_path: PathBuf,
log_salt: Arc<[u8; 32]>,
}

impl UnifiedPushService {
pub fn new(config: Config, client: Arc<reqwest::Client>) -> Self {
let storage_path = PathBuf::from("data/unifiedpush_endpoints.json");
let mut salt_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut salt_bytes);

Self {
config,
client,
endpoints: RwLock::new(HashMap::new()),
storage_path,
log_salt: Arc::new(salt_bytes),
}
}

fn log_device_id(&self, device_id: &str) -> String {
log_unifiedpush_identifier(self.log_salt.as_ref(), device_id)
}

/// Load endpoints from disk on startup
pub async fn load_endpoints(&self) -> Result<(), Box<dyn std::error::Error>> {
// Create data directory if it doesn't exist
Expand Down Expand Up @@ -107,7 +117,10 @@ impl UnifiedPushService {
// Persist to disk
self.save_endpoints().await?;

info!("Registered UnifiedPush endpoint for device: {}", device_id);
info!(
"Registered UnifiedPush endpoint device={}",
self.log_device_id(&device_id)
);
Ok(())
}

Expand All @@ -126,13 +139,17 @@ impl UnifiedPushService {
self.save_endpoints().await?;

info!(
"Unregistered UnifiedPush endpoint for device: {}",
device_id
"Unregistered UnifiedPush endpoint device={}",
self.log_device_id(device_id)
);
Ok(())
}
}

fn log_unifiedpush_identifier(salt: &[u8; 32], value: &str) -> String {
log_pubkey(salt, value)
}

#[async_trait]
impl PushService for UnifiedPushService {
async fn send_to_token(
Expand All @@ -146,10 +163,7 @@ impl PushService for UnifiedPushService {
"timestamp": chrono::Utc::now().timestamp()
});

debug!(
"Sending UnifiedPush to endpoint: {}...",
&device_token[..30.min(device_token.len())]
);
debug!("Sending UnifiedPush notification");

let response = self.client.post(device_token).json(&payload).send().await?;

Expand All @@ -169,3 +183,25 @@ impl PushService for UnifiedPushService {
matches!(platform, Platform::Android)
}
}

#[cfg(test)]
mod tests {
use super::log_unifiedpush_identifier;

#[test]
fn unifiedpush_log_identifier_is_stable_and_redacted() {
let salt = [7u8; 32];
let device_id = "device-secret-123";

let first = log_unifiedpush_identifier(&salt, device_id);
let second = log_unifiedpush_identifier(&salt, device_id);

assert_eq!(first, second);
assert_eq!(first.len(), 8);
assert_ne!(first, device_id);
assert!(
!device_id.contains(&first),
"hashed log identifier must not be a raw device_id substring"
);
}
}
Loading