Skip to content
Merged
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
33 changes: 33 additions & 0 deletions contracts/health-records/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub enum DataKey {
CategoryIndex(RecordCategory), // category -> Vec<u64> of record ids in that category, for prefix-style queries
ProviderRegistry,
RecordVersion(u64, u32),
/// Per-caller nonce for replay attack protection: (caller) -> u64
CallerNonce(Address),
}

#[contracterror]
Expand All @@ -94,6 +96,7 @@ pub enum Error {
BatchTooLarge = 7,
ProviderNotRegistered = 8,
VersionNotFound = 9,
StaleNonce = 10,
}

#[soroban_sdk::contractclient(name = "ProviderRegistryClient")]
Expand Down Expand Up @@ -571,6 +574,36 @@ impl HealthRecords {
) -> Vec<u64> {
shared_get_by_corr(&env, correlation_id)
}

/// Verify and increment caller's nonce for cross-contract call protection.
/// Returns an error if the provided nonce is <= the last successful nonce.
fn verify_and_increment_nonce(
env: &Env,
caller: &Address,
provided_nonce: u64,
) -> Result<(), Error> {
let nonce_key = DataKey::CallerNonce(caller.clone());
let last_nonce: u64 = env
.storage()
.persistent()
.get(&nonce_key)
.unwrap_or(0);

// Reject if provided nonce is not strictly greater than last successful nonce
if provided_nonce <= last_nonce {
return Err(Error::StaleNonce);
}

// Update nonce to prevent replay
env.storage().persistent().set(&nonce_key, &provided_nonce);
Ok(())
}

/// Get the current nonce for a caller.
pub fn get_caller_nonce(env: Env, caller: Address) -> u64 {
let nonce_key = DataKey::CallerNonce(caller);
env.storage().persistent().get(&nonce_key).unwrap_or(0)
}
}

#[cfg(test)]
Expand Down
62 changes: 53 additions & 9 deletions contracts/hospital-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub enum ContractError {
EmptyFieldUpdate = 6,
InvalidAddress = 7,
ConfigLimitExceeded = 8,
NotAuthorized = 9,
}

#[contracttype]
Expand Down Expand Up @@ -144,6 +145,7 @@ pub struct AuditEvent {
pub enum DataKey {
Hospital(Address),
HospitalConfig(Address),
Admin,
}

#[contract]
Expand Down Expand Up @@ -210,6 +212,48 @@ impl HospitalRegistry {
.publish((symbol_short!("audit"), caller.clone()), event);
}

/// Check if caller is authorized to modify the hospital's config.
/// Authorized if: caller is the hospital OR caller is the admin.
fn assert_config_auth(
env: &Env,
caller: &Address,
hospital_address: &Address,
) -> Result<(), ContractError> {
caller.require_auth();

// Hospital representative can update their own config
if caller == hospital_address {
return Ok(());
}

// Admin can update any hospital config
if let Some(admin) = env.storage().persistent().get::<_, Address>(&DataKey::Admin) {
if caller == &admin {
return Ok(());
}
}

Err(ContractError::NotAuthorized)
}

/// Set the admin address. Only callable by the current admin (if set) or initially.
pub fn set_admin(env: Env, caller: Address, admin: Address) -> Result<(), ContractError> {
validate_nonzero_address(&admin).map_err(|_| ContractError::InvalidAddress)?;
caller.require_auth();

// Check if admin is already set
if let Some(current_admin) = env.storage().persistent().get::<_, Address>(&DataKey::Admin) {
// Only the current admin can change the admin
if caller != current_admin {
return Err(ContractError::NotAuthorized);
}
}
// If no admin is set yet, the caller can set themselves as admin (first-time init)

env.storage().persistent().set(&DataKey::Admin, &admin);
Ok(())
}

pub fn register_hospital(
env: Env,
wallet: Address,
Expand Down Expand Up @@ -294,7 +338,7 @@ impl HospitalRegistry {
config: HospitalConfig,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;

if config.departments.len() > MAX_DEPARTMENTS {
Expand Down Expand Up @@ -335,7 +379,7 @@ impl HospitalRegistry {
departments: Vec<Department>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
if departments.is_empty() && !config.departments.is_empty() {
Expand All @@ -359,7 +403,7 @@ impl HospitalRegistry {
locations: Vec<Location>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
if locations.is_empty() && !config.locations.is_empty() {
Expand All @@ -383,7 +427,7 @@ impl HospitalRegistry {
equipment: Vec<EquipmentResource>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
if equipment.is_empty() && !config.equipment.is_empty() {
Expand All @@ -407,7 +451,7 @@ impl HospitalRegistry {
policies: Vec<PolicyProcedure>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
let old = config.clone();
Expand All @@ -425,7 +469,7 @@ impl HospitalRegistry {
alerts: Vec<AlertSetting>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
let old = config.clone();
Expand All @@ -443,7 +487,7 @@ impl HospitalRegistry {
insurance_providers: Vec<InsuranceProviderConfig>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
let old = config.clone();
Expand All @@ -461,7 +505,7 @@ impl HospitalRegistry {
billing: BillingConfig,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
let old = config.clone();
Expand All @@ -479,7 +523,7 @@ impl HospitalRegistry {
protocols: Vec<EmergencyProtocol>,
) -> Result<(), ContractError> {
validate_nonzero_address(&wallet).map_err(|_| ContractError::InvalidAddress)?;
wallet.require_auth();
Self::assert_config_auth(&env, &wallet, &wallet)?;
Self::assert_active_hospital(&env, &wallet)?;
let mut config = Self::get_hospital_config(env.clone(), wallet.clone())?;
let old = config.clone();
Expand Down
33 changes: 33 additions & 0 deletions contracts/patient-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ pub enum DataKey {
MerkleRoot(Address),
/// Optional provider-registry contract address for cross-contract verification.
ProviderRegistry,
/// Per-caller nonce for replay attack protection: (caller) -> u64
CallerNonce(Address),
}

/// --------------------
Expand Down Expand Up @@ -292,6 +294,7 @@ pub enum ContractError {
ProviderNotRegistered = 28,
/// A Vec parameter or accumulator exceeds its maximum allowed length.
InputTooLarge = 29,
StaleNonce = 30,
}

pub fn validate_cid(cid: &Bytes) -> Result<(), ContractError> {
Expand Down Expand Up @@ -2778,6 +2781,36 @@ impl MedicalRegistry {
extend_for_retention_class(env, key, &class);
}
}

/// Verify and increment caller's nonce for cross-contract call protection.
/// Returns an error if the provided nonce is <= the last successful nonce.
fn verify_and_increment_nonce(
env: &Env,
caller: &Address,
provided_nonce: u64,
) -> Result<(), ContractError> {
let nonce_key = DataKey::CallerNonce(caller.clone());
let last_nonce: u64 = env
.storage()
.persistent()
.get(&nonce_key)
.unwrap_or(0);

// Reject if provided nonce is not strictly greater than last successful nonce
if provided_nonce <= last_nonce {
return Err(ContractError::StaleNonce);
}

// Update nonce to prevent replay
env.storage().persistent().set(&nonce_key, &provided_nonce);
Ok(())
}

/// Get the current nonce for a caller.
pub fn get_caller_nonce(env: Env, caller: Address) -> u64 {
let nonce_key = DataKey::CallerNonce(caller);
env.storage().persistent().get(&nonce_key).unwrap_or(0)
}
}

#[cfg(test)]
Expand Down
46 changes: 30 additions & 16 deletions contracts/prescription-management/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ pub enum Error {
MissingRecallReason = 28,
/// Patient already has MAX_ACTIVE_PRESCRIPTIONS active prescriptions
TooManyActivePrescriptions = 29,
StaleNonce = 30,
}

#[contracttype]
Expand Down Expand Up @@ -258,6 +259,8 @@ pub enum DataKey {
/// `count_and_prune_active_prescriptions`), so it stays close to the
/// patient's actual active count rather than growing unboundedly.
PatientPrescriptions(Address),
/// Per-caller nonce for replay attack protection: (caller) -> u64
CallerNonce(Address),
}

#[contracttype]
Expand Down Expand Up @@ -1567,23 +1570,34 @@ impl PrescriptionContract {
false
}

/// Retrieve the medication names of all active prescriptions for a patient.
/// Used by cross-contract callers (e.g. nutrition-care-management) to check
/// for drug-nutrient contraindications.
pub fn get_patient_active_prescriptions(env: Env, patient: Address) -> Vec<String> {
let now = env.ledger().timestamp();
let key = DataKey::PatientPrescriptions(patient);
let ids: Vec<u64> = env.storage().persistent().get(&key).unwrap_or(Vec::new(&env));

let mut active_meds: Vec<String> = Vec::new(&env);
for id in ids.iter() {
if let Some(prescription) = env.storage().persistent().get::<_, Prescription>(&id) {
if is_prescription_active(&prescription, now) {
active_meds.push_back(prescription.medication_name);
}
}
/// Verify and increment caller's nonce for cross-contract call protection.
/// Returns an error if the provided nonce is <= the last successful nonce.
fn verify_and_increment_nonce(
env: &Env,
caller: &Address,
provided_nonce: u64,
) -> Result<(), Error> {
let nonce_key = DataKey::CallerNonce(caller.clone());
let last_nonce: u64 = env
.storage()
.persistent()
.get(&nonce_key)
.unwrap_or(0);

// Reject if provided nonce is not strictly greater than last successful nonce
if provided_nonce <= last_nonce {
return Err(Error::StaleNonce);
}
active_meds

// Update nonce to prevent replay
env.storage().persistent().set(&nonce_key, &provided_nonce);
Ok(())
}

/// Get the current nonce for a caller.
pub fn get_caller_nonce(env: Env, caller: Address) -> u64 {
let nonce_key = DataKey::CallerNonce(caller);
env.storage().persistent().get(&nonce_key).unwrap_or(0)
}
}

Expand Down
33 changes: 33 additions & 0 deletions contracts/provider-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum Error {
NoRotationPending = 9,
RotationExpired = 10,
NotPendingAdmin = 11,
StaleNonce = 12,
}

/// Input entry for `batch_register_providers`.
Expand Down Expand Up @@ -95,6 +96,8 @@ pub enum DataKey {
ProviderRatingByPatient(Address, Address),
PendingAdmin,
RotationExpiry,
/// Per-caller nonce for replay attack protection: (caller) -> u64
CallerNonce(Address),
}

// ── Contract ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -344,4 +347,34 @@ impl ProviderRegistry {
}
Ok(())
}

/// Verify and increment caller's nonce for cross-contract call protection.
/// Returns an error if the provided nonce is <= the last successful nonce.
fn verify_and_increment_nonce(
env: &Env,
caller: &Address,
provided_nonce: u64,
) -> Result<(), Error> {
let nonce_key = DataKey::CallerNonce(caller.clone());
let last_nonce: u64 = env
.storage()
.persistent()
.get(&nonce_key)
.unwrap_or(0);

// Reject if provided nonce is not strictly greater than last successful nonce
if provided_nonce <= last_nonce {
return Err(Error::StaleNonce);
}

// Update nonce to prevent replay
env.storage().persistent().set(&nonce_key, &provided_nonce);
Ok(())
}

/// Get the current nonce for a caller.
pub fn get_caller_nonce(env: Env, caller: Address) -> u64 {
let nonce_key = DataKey::CallerNonce(caller);
env.storage().persistent().get(&nonce_key).unwrap_or(0)
}
}
Loading
Loading