mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
109 lines
3.5 KiB
Rust
109 lines
3.5 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use url::Url;
|
|
|
|
pub(crate) const AUDIENCE: &str = "buzz:nostr-identity";
|
|
pub(crate) const ACTION: &str = "bind_nostr_identity";
|
|
pub(crate) const CONTENT: &str = "";
|
|
pub(crate) const KIND: u16 = buzz_core_pkg::kind::KIND_NOSTR_IDENTITY_BINDING as u16;
|
|
pub(crate) const PROTOCOL: &str = "buzz-nostr-identity";
|
|
pub(crate) const RETURN_MODE_CLIPBOARD: &str = "clipboard";
|
|
pub(crate) const RETURN_MODE_BROWSER_FRAGMENT_V1: &str = "browser_fragment_v1";
|
|
pub(crate) const VERSION: &str = "1";
|
|
|
|
const NONCE_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
const VERIFICATION_CODE_LENGTH: usize = 6;
|
|
|
|
pub(crate) fn validate_challenge_id(challenge_id: &str) -> Result<(), String> {
|
|
if challenge_id.is_empty() {
|
|
return Err("challenge_id is required".into());
|
|
}
|
|
uuid::Uuid::parse_str(challenge_id).map_err(|_| "invalid challenge_id".to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_nonce(nonce: &str) -> Result<(), String> {
|
|
if nonce.is_empty() {
|
|
return Err("nonce is required".into());
|
|
}
|
|
if nonce.len() != 43 || !nonce.chars().all(|ch| NONCE_CHARS.contains(ch)) {
|
|
return Err("invalid nonce".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_verification_code(verification_code: &str) -> Result<(), String> {
|
|
if verification_code.len() != VERIFICATION_CODE_LENGTH
|
|
|| !verification_code.bytes().all(|byte| byte.is_ascii_digit())
|
|
{
|
|
return Err("verification_code must be exactly 6 digits".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_protocol_fields(
|
|
audience: &str,
|
|
action: &str,
|
|
protocol: &str,
|
|
version: &str,
|
|
) -> Result<(), String> {
|
|
if audience != AUDIENCE {
|
|
return Err("unsupported audience".into());
|
|
}
|
|
if action != ACTION {
|
|
return Err("unsupported action".into());
|
|
}
|
|
if protocol != PROTOCOL {
|
|
return Err("unsupported protocol".into());
|
|
}
|
|
if version != VERSION {
|
|
return Err("unsupported version".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_origin(origin: &str) -> Result<(), String> {
|
|
let parsed = Url::parse(origin).map_err(|error| format!("invalid origin: {error}"))?;
|
|
if parsed.scheme() != "https" {
|
|
return Err("origin must use https".into());
|
|
}
|
|
if parsed.host_str().is_none() {
|
|
return Err("origin missing host".into());
|
|
}
|
|
if !parsed.username().is_empty() || parsed.password().is_some() {
|
|
return Err("origin must not include credentials".into());
|
|
}
|
|
if parsed.path() != "/" || parsed.query().is_some() || parsed.fragment().is_some() {
|
|
return Err("origin must not include path, query, or fragment".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_expires_at_format(expires_at: &str) -> Result<DateTime<Utc>, String> {
|
|
DateTime::parse_from_rfc3339(expires_at)
|
|
.map_err(|error| format!("invalid expires_at: {error}"))
|
|
.map(|parsed| parsed.with_timezone(&Utc))
|
|
}
|
|
|
|
pub(crate) fn validate_expires_at(expires_at: &str) -> Result<(), String> {
|
|
let parsed = validate_expires_at_format(expires_at)?;
|
|
if parsed <= Utc::now() {
|
|
return Err("expires_at is expired".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_signing_request(
|
|
challenge_id: &str,
|
|
nonce: &str,
|
|
verification_code: &str,
|
|
origin: &str,
|
|
expires_at: &str,
|
|
) -> Result<(), String> {
|
|
validate_challenge_id(challenge_id)?;
|
|
validate_nonce(nonce)?;
|
|
validate_verification_code(verification_code)?;
|
|
validate_origin(origin)?;
|
|
validate_expires_at(expires_at)?;
|
|
Ok(())
|
|
}
|