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
58 changes: 54 additions & 4 deletions backend/src/secure_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const KEY_LEN: usize = 32;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateLegacyMessageRequest {
pub vault_id: Option<i64>,
pub beneficiary_contact: String,
pub message: String,
pub unlock_at: DateTime<Utc>,
Expand All @@ -24,6 +25,7 @@ pub struct CreateLegacyMessageRequest {
pub struct LegacyMessage {
pub id: Uuid,
pub owner_user_id: Uuid,
pub vault_id: Option<i64>,
pub beneficiary_contact: String,
pub key_version: i32,
pub unlock_at: DateTime<Utc>,
Expand Down Expand Up @@ -324,6 +326,7 @@ impl MessageEncryptionService {
struct Row {
id: Uuid,
owner_user_id: Uuid,
vault_id: Option<i64>,
beneficiary_contact: String,
key_version: i32,
unlock_at: DateTime<Utc>,
Expand All @@ -334,11 +337,12 @@ impl MessageEncryptionService {

let row = sqlx::query_as::<_, Row>(
"INSERT INTO legacy_messages \
(owner_user_id, beneficiary_contact, encrypted_payload, payload_nonce, key_version, unlock_at, status) \
VALUES ($1, $2, $3, $4, $5, $6, 'pending') \
RETURNING id, owner_user_id, beneficiary_contact, key_version, unlock_at, status, delivered_at, created_at",
(owner_user_id, vault_id, beneficiary_contact, encrypted_payload, payload_nonce, key_version, unlock_at, status) \
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending') \
RETURNING id, owner_user_id, vault_id, beneficiary_contact, key_version, unlock_at, status, delivered_at, created_at",
)
.bind(owner_user_id)
.bind(req.vault_id)
.bind(req.beneficiary_contact.trim())
.bind(&encrypted_payload)
.bind(&payload_nonce)
Expand All @@ -350,6 +354,7 @@ impl MessageEncryptionService {
Ok(LegacyMessage {
id: row.id,
owner_user_id: row.owner_user_id,
vault_id: row.vault_id,
beneficiary_contact: row.beneficiary_contact,
key_version: row.key_version,
unlock_at: row.unlock_at,
Expand All @@ -367,6 +372,7 @@ impl MessageEncryptionService {
struct Row {
id: Uuid,
owner_user_id: Uuid,
vault_id: Option<i64>,
beneficiary_contact: String,
key_version: i32,
unlock_at: DateTime<Utc>,
Expand All @@ -376,7 +382,7 @@ impl MessageEncryptionService {
}

let rows = sqlx::query_as::<_, Row>(
"SELECT id, owner_user_id, beneficiary_contact, key_version, unlock_at, status, delivered_at, created_at \
"SELECT id, owner_user_id, vault_id, beneficiary_contact, key_version, unlock_at, status, delivered_at, created_at \
FROM legacy_messages WHERE owner_user_id = $1 ORDER BY created_at DESC",
)
.bind(owner_user_id)
Expand All @@ -388,6 +394,50 @@ impl MessageEncryptionService {
.map(|r| LegacyMessage {
id: r.id,
owner_user_id: r.owner_user_id,
vault_id: r.vault_id,
beneficiary_contact: r.beneficiary_contact,
key_version: r.key_version,
unlock_at: r.unlock_at,
status: r.status,
delivered_at: r.delivered_at,
created_at: r.created_at,
})
.collect())
}

pub async fn list_vault_messages(
db: &PgPool,
owner_user_id: Uuid,
vault_id: i64,
) -> Result<Vec<LegacyMessage>, ApiError> {
#[derive(sqlx::FromRow)]
struct Row {
id: Uuid,
owner_user_id: Uuid,
vault_id: Option<i64>,
beneficiary_contact: String,
key_version: i32,
unlock_at: DateTime<Utc>,
status: String,
delivered_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc>,
}

let rows = sqlx::query_as::<_, Row>(
"SELECT id, owner_user_id, vault_id, beneficiary_contact, key_version, unlock_at, status, delivered_at, created_at \
FROM legacy_messages WHERE owner_user_id = $1 AND vault_id = $2 ORDER BY created_at DESC",
)
.bind(owner_user_id)
.bind(vault_id)
.fetch_all(db)
.await?;

Ok(rows
.into_iter()
.map(|r| LegacyMessage {
id: r.id,
owner_user_id: r.owner_user_id,
vault_id: r.vault_id,
beneficiary_contact: r.beneficiary_contact,
key_version: r.key_version,
unlock_at: r.unlock_at,
Expand Down
2 changes: 0 additions & 2 deletions contracts/inheritance-contract/src/message_test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(test)]

use super::*;
use mock_token::MockToken;
use soroban_sdk::{
Expand Down
107 changes: 107 additions & 0 deletions contracts/inheritance-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4321,6 +4321,113 @@ fn test_finalized_version_is_immutable() {
assert!(client.is_will_finalized(&plan_id, &version));
}

// --- Issue #360: Message Update Before Lock ---

#[test]
fn test_update_legacy_message_before_lock() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);

let original_hash = BytesN::from_array(&env, &[1u8; 32]);
let future_ts = env.ledger().timestamp() + 10_000;
let message_id = client.create_legacy_message(
&owner,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: original_hash,
unlock_timestamp: future_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_1"),
},
);

let updated_hash = BytesN::from_array(&env, &[2u8; 32]);
let new_unlock_ts = future_ts + 5_000;
client.update_legacy_message(
&owner,
&message_id,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: updated_hash.clone(),
unlock_timestamp: new_unlock_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_updated"),
},
);

let stored = client.get_legacy_message(&message_id).unwrap();
assert_eq!(stored.message_hash, updated_hash);
assert_eq!(stored.unlock_timestamp, new_unlock_ts);
assert!(!stored.is_finalized);
}

#[test]
fn test_update_legacy_message_rejected_after_lock() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);

let future_ts = env.ledger().timestamp() + 10_000;
let message_id = client.create_legacy_message(
&owner,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: BytesN::from_array(&env, &[1u8; 32]),
unlock_timestamp: future_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_1"),
},
);

// Finalize (lock) the message
client.finalize_legacy_message(&owner, &message_id);
assert!(client.get_legacy_message(&message_id).unwrap().is_finalized);

// Update after finalization must fail
let result = client.try_update_legacy_message(
&owner,
&message_id,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: BytesN::from_array(&env, &[3u8; 32]),
unlock_timestamp: future_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_new"),
},
);
assert_eq!(result, Err(Ok(InheritanceError::WillAlreadyFinalized)));
}

#[test]
fn test_update_legacy_message_unauthorized() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);

let future_ts = env.ledger().timestamp() + 10_000;
let message_id = client.create_legacy_message(
&owner,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: BytesN::from_array(&env, &[1u8; 32]),
unlock_timestamp: future_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_1"),
},
);

let stranger = Address::generate(&env);
let result = client.try_update_legacy_message(
&stranger,
&message_id,
&CreateLegacyMessageParams {
vault_id: plan_id,
message_hash: BytesN::from_array(&env, &[9u8; 32]),
unlock_timestamp: future_ts,
key_reference: soroban_sdk::String::from_str(&env, "ref_9"),
},
);
assert_eq!(result, Err(Ok(InheritanceError::Unauthorized)));
}
// --- Issue #71: KYC Verification for Plan Creation and Claiming ---

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1888,7 +1888,7 @@
"data": {
"vec": [
{
"string": "caught panic 'called `Result::unwrap()` on an `Err` value: ConversionError' from contract function 'Symbol(obj#407)'"
"string": "caught panic 'called `Result::unwrap()` on an `Err` value: ConversionError' from contract function 'Symbol(obj#417)'"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
Expand Down
Loading
Loading