Skip to main content

qualia_core_db/identity/
agency.rs

1use crate::NQuin;
2use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
3use sha2::{Digest, Sha256};
4
5#[derive(Debug, PartialEq, Eq)]
6pub enum AgencyError {
7    InvalidSignature,
8    TamperedData,
9}
10
11/// Computes the Author-Scoped Merkle Sub-Root Hash for a specific user's claims.
12/// It partitions the 128KB frame by the Author's DID, strictly ignoring claims
13/// authored by other actors in the Bilateral frame.
14/// Uses zero-allocation iteration over the existing memory slice.
15pub fn compute_scoped_merkle_root(frame: &[NQuin], author_did: u64) -> [u8; 32] {
16    let mut hasher = Sha256::new();
17
18    // Iterate through the frame without allocating Vectors or Strings
19    for quin in frame.iter() {
20        // In Qualia-DB, the Author's DID is embedded in the Context vector
21        if quin.context == author_did {
22            // Hash the 48-byte Quin structure natively
23            // `bytemuck::bytes_of` safely casts the struct to a byte slice since it implements Pod.
24            let bytes: &[u8; 48] = bytemuck::cast_ref(quin);
25            hasher.update(bytes);
26        }
27    }
28
29    // Finalize the Merkle Sub-Root hash (32 bytes)
30    let result = hasher.finalize();
31    let mut root_hash = [0u8; 32];
32    root_hash.copy_from_slice(&result);
33    root_hash
34}
35
36/// The Human Agency Hook
37/// Generates a 64-byte Ed25519 signature exclusively over the Author-Scoped Merkle Sub-Root.
38pub fn sign_agency_root(signing_key: &SigningKey, sub_root_hash: &[u8; 32]) -> Signature {
39    // The Ed25519-dalek library natively signs raw byte arrays.
40    signing_key.sign(sub_root_hash)
41}
42
43/// The Verification Gate
44/// Validates an incoming 64-byte signature against the author's Public Key (`VerifyingKey`).
45/// Only validates the specific claims matching the author's DID, ensuring Bilateral Integrity.
46pub fn verify_human_agency(
47    frame: &[NQuin],
48    author_did: u64,
49    verifying_key: &VerifyingKey,
50    signature: &Signature,
51) -> Result<(), AgencyError> {
52    // 1. Recompute the Author-Scoped Merkle Sub-Root from the incoming frame
53    let expected_sub_root = compute_scoped_merkle_root(frame, author_did);
54
55    // 2. Validate the signature mathematically
56    if verifying_key.verify(&expected_sub_root, signature).is_ok() {
57        Ok(())
58    } else {
59        Err(AgencyError::InvalidSignature)
60    }
61}
62
63/// Stamp fiduciary metadata and refresh the XOR parity block before WAL commit.
64/// `principal_did_hash` is embedded in `context`; agent identity in metadata low bits.
65pub fn stamp_fiduciary_metadata(quin: &mut NQuin, principal_did_hash: u64, agent_did_hash: u64) {
66    quin.context = principal_did_hash;
67    let agent_lane = agent_did_hash & 0xFFFF;
68    let principal_clock = (principal_did_hash >> 16) & 0x1FFF_FFFF;
69    quin.metadata = agent_lane | (principal_clock << 16);
70    quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context ^ quin.metadata;
71}
72
73/// Volatile zero of all Quin fields after WAL commit (wipes transient LLM state).
74pub fn scrub_quin_volatile(quin: &mut NQuin) {
75    unsafe {
76        std::ptr::write_volatile(&mut quin.subject, 0);
77        std::ptr::write_volatile(&mut quin.predicate, 0);
78        std::ptr::write_volatile(&mut quin.object, 0);
79        std::ptr::write_volatile(&mut quin.context, 0);
80        std::ptr::write_volatile(&mut quin.metadata, 0);
81        std::ptr::write_volatile(&mut quin.parity, 0);
82    }
83}
84
85/// Sign a single graph-mutation Quin using the author-scoped Merkle sub-root.
86pub fn sign_graph_mutation(signing_key: &SigningKey, quin: &NQuin) -> Signature {
87    let frame = [*quin];
88    let root = compute_scoped_merkle_root(&frame, quin.context);
89    sign_agency_root(signing_key, &root)
90}
91
92/// Derives a 32-byte AES-256-GCM key from the user's PIN for Deniable Encryption (Sanctuary Mode).
93/// By passing different PINs, different keys are derived, which unlocks different DB Lanes.
94/// The decoy lane operates exactly identically to the sanctuary lane.
95#[cfg(all(feature = "sanctuary-crypto", not(target_arch = "wasm32")))]
96const LANE_KEY_LENGTH: usize = 32;
97
98#[cfg(all(feature = "sanctuary-crypto", not(target_arch = "wasm32")))]
99pub fn derive_lane_key(pin: &str, salt: &[u8]) -> [u8; LANE_KEY_LENGTH] {
100    crate::sanctuary_crypto::derive_lane_cipher_key(
101        pin.as_bytes(),
102        salt,
103        crate::sanctuary_crypto::DEFAULT_PBKDF2_ITERATIONS,
104    )
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_human_agency_verification() {
113        // Use a static 32-byte secret for the test to avoid pulling in rand_core
114        let secret = [42u8; 32];
115        let signing_key = SigningKey::from_bytes(&secret);
116        let verifying_key: VerifyingKey = signing_key.verifying_key();
117
118        let author_did_alice = 1001;
119        let author_did_bob = 2002;
120
121        let mut frame = [NQuin {
122            subject: 0,
123            predicate: 0,
124            object: 0,
125            context: 0,
126            metadata: 0,
127            parity: 0,
128        }; 10];
129
130        // Alice's claims
131        frame[0].context = author_did_alice;
132        frame[0].subject = 55;
133
134        frame[1].context = author_did_alice;
135        frame[1].subject = 66;
136
137        // Bob's claims (injected into the same bilateral frame)
138        frame[2].context = author_did_bob;
139        frame[2].subject = 99;
140
141        // 1. Alice computes her scoped root and signs it
142        let alice_root = compute_scoped_merkle_root(&frame, author_did_alice);
143        let alice_sig = sign_agency_root(&signing_key, &alice_root);
144
145        // 2. Verification Gate validates Alice's signature successfully
146        assert_eq!(
147            verify_human_agency(&frame, author_did_alice, &verifying_key, &alice_sig),
148            Ok(())
149        );
150
151        // 3. Tampering simulation: Someone alters Alice's claim
152        frame[0].subject = 999;
153        assert_eq!(
154            verify_human_agency(&frame, author_did_alice, &verifying_key, &alice_sig),
155            Err(AgencyError::InvalidSignature)
156        );
157    }
158
159    #[cfg(all(feature = "sanctuary-crypto", not(target_arch = "wasm32")))]
160    #[test]
161    fn derive_lane_key_is_deterministic_and_salt_bound() {
162        let pin = "1234";
163        let salt_a = b"sanctuary";
164        let salt_b = b"decoy";
165
166        let key_a1 = derive_lane_key(pin, salt_a);
167        let key_a2 = derive_lane_key(pin, salt_a);
168        let key_b = derive_lane_key(pin, salt_b);
169        let key_c = derive_lane_key("4321", salt_a);
170
171        assert_eq!(key_a1, key_a2);
172        assert_ne!(key_a1, key_b);
173        assert_ne!(key_a1, key_c);
174        assert_eq!(
175            key_a1,
176            crate::sanctuary_crypto::derive_lane_cipher_key(
177                pin.as_bytes(),
178                salt_a,
179                crate::sanctuary_crypto::DEFAULT_PBKDF2_ITERATIONS,
180            )
181        );
182    }
183}