use serde_json::{Map, Value, json};
use crate::crypto::ed25519;
use crate::crypto::types::CryptoError;
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("sign failed: {0}")]
Sign(#[from] CryptoError),
}
type AuthResult<T> = Result<T, AuthError>;
#[allow(missing_docs)]
pub mod namespace {
pub const DEFAULT: i32 = 0;
pub const USER_PROFILE: i32 = 2;
pub const USER_CONTACTS: i32 = 3;
pub const USER_GROUPS: i32 = 5;
pub const CONVO_INFO_VOLATILE: i32 = 4;
pub const CLOSED_GROUP_MESSAGES: i32 = -10;
pub const GROUP_MESSAGES: i32 = 11;
pub const GROUP_KEYS: i32 = 12;
pub const GROUP_INFO: i32 = 13;
pub const GROUP_MEMBERS: i32 = 14;
pub const REVOKED_RETRIEVABLE_GROUP_MESSAGES: i32 = -11;
pub const CONFIG_PUSH_NOTIFICATIONS: i32 = -12;
}
pub fn namespace_signature_fragment(namespace: i32) -> String {
if namespace == 0 {
String::new()
} else {
namespace.to_string()
}
}
pub fn canonical_sign_string(method: &str, namespace: i32, timestamp_ms: u64) -> String {
format!(
"{}{}{}",
method,
namespace_signature_fragment(namespace),
timestamp_ms
)
}
pub fn sign_request_b64(
method: &str,
namespace: i32,
timestamp_ms: u64,
privkey: &[u8],
) -> AuthResult<String> {
let msg = canonical_sign_string(method, namespace, timestamp_ms);
let sig = ed25519::sign(privkey, msg.as_bytes())?;
Ok(base64_standard(&sig))
}
pub fn build_auth_fields(
method: &str,
pubkey_05: &str,
pubkey_ed: &str,
namespace: i32,
timestamp_ms: u64,
privkey: &[u8],
) -> AuthResult<Map<String, Value>> {
let sig_b64 = sign_request_b64(method, namespace, timestamp_ms, privkey)?;
let mut m = Map::new();
m.insert("pubkey".to_string(), json!(pubkey_05));
m.insert("pubkey_ed25519".to_string(), json!(pubkey_ed));
m.insert("timestamp".to_string(), json!(timestamp_ms));
if namespace != 0 {
m.insert("namespace".to_string(), json!(namespace));
}
m.insert("signature".to_string(), json!(sig_b64));
Ok(m)
}
pub fn current_timestamp_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn base64_standard(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
#[test]
fn test_namespace_fragment_zero_is_empty() {
assert_eq!(namespace_signature_fragment(0), "");
}
#[test]
fn test_namespace_fragment_positive() {
assert_eq!(namespace_signature_fragment(11), "11");
assert_eq!(namespace_signature_fragment(999), "999");
}
#[test]
fn test_namespace_fragment_negative() {
assert_eq!(namespace_signature_fragment(-10), "-10");
assert_eq!(namespace_signature_fragment(-11), "-11");
}
#[test]
fn test_canonical_string_shape() {
assert_eq!(
canonical_sign_string("retrieve", 0, 1_700_000_000_000),
"retrieve1700000000000"
);
assert_eq!(
canonical_sign_string("retrieve", 11, 1_700_000_000_000),
"retrieve111700000000000"
);
assert_eq!(
canonical_sign_string("store", -10, 1_700_000_000_000),
"store-101700000000000"
);
}
#[test]
fn test_sign_is_deterministic_for_same_inputs() {
let seed = hex!(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
);
let a = sign_request_b64("retrieve", 11, 1_700_000_000_000, &seed).unwrap();
let b = sign_request_b64("retrieve", 11, 1_700_000_000_000, &seed).unwrap();
assert_eq!(a, b);
use base64::Engine;
let raw = base64::engine::general_purpose::STANDARD.decode(&a).unwrap();
assert_eq!(raw.len(), 64);
}
#[test]
fn test_sign_differs_on_namespace() {
let seed = hex!(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
);
let a = sign_request_b64("retrieve", 0, 1_700_000_000_000, &seed).unwrap();
let b = sign_request_b64("retrieve", 11, 1_700_000_000_000, &seed).unwrap();
assert_ne!(a, b);
}
#[test]
fn test_sign_round_trips_with_verify() {
let seed = hex!(
"1111111111111111111111111111111111111111111111111111111111111111"
);
let (pk, _sk) = ed25519::ed25519_key_pair_from_seed(&seed).unwrap();
let msg = canonical_sign_string("retrieve", 11, 1_700_000_000_000);
let sig_b64 =
sign_request_b64("retrieve", 11, 1_700_000_000_000, &seed).unwrap();
use base64::Engine;
let sig = base64::engine::general_purpose::STANDARD.decode(&sig_b64).unwrap();
assert!(ed25519::verify(&sig, &pk, msg.as_bytes()).unwrap());
}
#[test]
fn test_build_auth_fields_omits_namespace_when_zero() {
let seed = [7u8; 32];
let m = build_auth_fields(
"retrieve",
"05aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
0,
1_700_000_000_000,
&seed,
)
.unwrap();
assert_eq!(m.get("pubkey").unwrap().as_str().unwrap().len(), 66);
assert!(m.contains_key("pubkey_ed25519"));
assert!(m.contains_key("signature"));
assert!(m.contains_key("timestamp"));
assert!(!m.contains_key("namespace"));
}
#[test]
fn test_build_auth_fields_includes_namespace_when_nonzero() {
let seed = [7u8; 32];
let m = build_auth_fields(
"retrieve",
"05aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
-10,
1_700_000_000_000,
&seed,
)
.unwrap();
assert_eq!(m.get("namespace").unwrap().as_i64().unwrap(), -10);
}
}