1use soroban_sdk::{Address, Env};
2
3use super::error::AccountError;
4use super::intent::{AuthMethod, AuthResult, IntentSigner, SignedIntent};
5use super::policy::{IntentContext, IntentExpiryPolicy, Policy, SessionContext, SessionPolicy};
6use super::replay::ReplayProtection;
7use super::signer::{AccountSigner, DirectAuthSigner, Secp256r1PasskeySigner, SessionAuthSigner};
8use super::storage::SessionStorage;
9
10pub struct AccountKernel {
12 owner: Address,
13}
14
15impl AccountKernel {
16 pub fn new(owner: Address) -> Self {
17 Self { owner }
18 }
19
20 pub fn owner(&self) -> &Address {
21 &self.owner
22 }
23
24 pub fn authorize_direct(
25 &self,
26 env: &Env,
27 intent: &SignedIntent,
28 ) -> Result<AuthResult, AccountError> {
29 self.ensure_target(intent)?;
30 self.ensure_hash(env, intent)?;
31
32 let signer = DirectAuthSigner;
33 signer.verify(env, &self.owner, intent)?;
34
35 let policy = IntentExpiryPolicy;
36 policy.evaluate(
37 env,
38 &IntentContext {
39 account: &self.owner,
40 intent,
41 },
42 )?;
43
44 let consumed =
45 ReplayProtection::verify_and_consume_account_nonce(env, &self.owner, intent.nonce)?;
46
47 Ok(AuthResult {
48 method: AuthMethod::Direct,
49 nonce_consumed: consumed,
50 session_key_id: zero_key(env),
51 remaining_operations: 0,
52 })
53 }
54
55 pub fn authorize_session(
56 &self,
57 env: &Env,
58 intent: &SignedIntent,
59 ) -> Result<AuthResult, AccountError> {
60 self.ensure_target(intent)?;
61 self.ensure_hash(env, intent)?;
62
63 let signer = SessionAuthSigner;
64 signer.verify(env, &self.owner, intent)?;
65
66 let policy = SessionPolicy;
67 policy.evaluate(
68 env,
69 &SessionContext {
70 account: &self.owner,
71 intent,
72 },
73 )?;
74
75 let updated = SessionStorage::consume_authorized_session(
76 env,
77 &self.owner,
78 &intent.signer.session_key_id,
79 )?;
80
81 Ok(AuthResult {
82 method: AuthMethod::Session,
83 nonce_consumed: updated.next_nonce - 1,
84 session_key_id: updated.key_id,
85 remaining_operations: updated.scope.max_operations - updated.operations_used,
86 })
87 }
88
89 pub fn authorize_passkey(
90 &self,
91 env: &Env,
92 intent: &SignedIntent,
93 ) -> Result<AuthResult, AccountError> {
94 self.ensure_target(intent)?;
95 self.ensure_hash(env, intent)?;
96
97 let signer = Secp256r1PasskeySigner;
98 signer.verify(env, &self.owner, intent)?;
99
100 let policy = IntentExpiryPolicy;
101 policy.evaluate(
102 env,
103 &IntentContext {
104 account: &self.owner,
105 intent,
106 },
107 )?;
108
109 let consumed =
110 ReplayProtection::verify_and_consume_account_nonce(env, &self.owner, intent.nonce)?;
111
112 Ok(AuthResult {
113 method: AuthMethod::Passkey,
114 nonce_consumed: consumed,
115 session_key_id: zero_key(env),
116 remaining_operations: 0,
117 })
118 }
119
120 pub fn authorize(&self, env: &Env, intent: &SignedIntent) -> Result<AuthResult, AccountError> {
121 match intent.signer.kind {
122 IntentSigner::Direct => self.authorize_direct(env, intent),
123 IntentSigner::Session => self.authorize_session(env, intent),
124 IntentSigner::Passkey => self.authorize_passkey(env, intent),
125 }
126 }
127
128 fn ensure_target(&self, intent: &SignedIntent) -> Result<(), AccountError> {
129 if intent.account != self.owner {
130 return Err(AccountError::Unauthorized);
131 }
132 Ok(())
133 }
134
135 fn ensure_hash(&self, env: &Env, intent: &SignedIntent) -> Result<(), AccountError> {
136 if intent.recompute_hash(env) != intent.action_hash {
137 return Err(AccountError::InvalidIntent);
138 }
139 Ok(())
140 }
141}
142
143fn zero_key(env: &Env) -> soroban_sdk::BytesN<32> {
144 soroban_sdk::BytesN::from_array(env, &[0u8; 32])
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::accounts::intent::SignedIntent;
151 use crate::accounts::multi_device::{DeviceManager, DevicePolicy, MultiDeviceProvider};
152 use crate::accounts::policy::{
153 ActiveDevicePolicy, DeviceContext, GuardianPolicy, RecoveryContext,
154 };
155 use crate::accounts::recovery::{RecoverableAccount, RecoveryConfig, RecoveryProvider};
156 use crate::accounts::storage::SessionStorage;
157 use crate::accounts::types::{GameAction, SessionKey, SessionScope};
158 use soroban_sdk::{
159 contract, contractimpl, symbol_short, testutils::Address as _, vec, Address, Bytes, BytesN,
160 Env,
161 };
162
163 #[contract]
164 pub struct TestContract;
165
166 #[contractimpl]
167 impl TestContract {}
168
169 fn make_action(env: &Env, name: &str) -> GameAction {
170 GameAction {
171 system_name: soroban_sdk::Symbol::new(env, name),
172 data: Bytes::new(env),
173 }
174 }
175
176 #[test]
177 fn test_direct_intent_consumes_account_nonce() {
178 let env = Env::default();
179 env.mock_all_auths();
180 let contract_id = env.register(TestContract, ());
181 let owner = Address::generate(&env);
182
183 env.as_contract(&contract_id, || {
184 let kernel = AccountKernel::new(owner.clone());
185 let action = make_action(&env, "move");
186 let intent = SignedIntent::direct(&env, owner, action, 0, 99999);
187
188 let result = kernel.authorize(&env, &intent).unwrap();
189 assert_eq!(result.method, AuthMethod::Direct);
190 assert_eq!(
191 ReplayProtection::next_account_nonce(&env, kernel.owner()),
192 1
193 );
194 });
195 }
196
197 #[test]
198 fn test_session_intent_enforces_scope_budget_and_nonce() {
199 let env = Env::default();
200 let contract_id = env.register(TestContract, ());
201 let owner = Address::generate(&env);
202 let key_id = BytesN::from_array(&env, &[7u8; 32]);
203
204 env.as_contract(&contract_id, || {
205 let kernel = AccountKernel::new(owner.clone());
206 let session = SessionKey {
207 key_id: key_id.clone(),
208 scope: SessionScope {
209 allowed_actions: vec![&env, symbol_short!("move")],
210 max_operations: 2,
211 expires_at: 99999,
212 },
213 created_at: 0,
214 operations_used: 0,
215 next_nonce: 0,
216 };
217 SessionStorage::store(&env, &owner, &session);
218
219 let move_1 = SignedIntent::session(
220 &env,
221 owner.clone(),
222 &key_id,
223 make_action(&env, "move"),
224 0,
225 99999,
226 );
227 let result_1 = kernel.authorize(&env, &move_1).unwrap();
228 assert_eq!(result_1.remaining_operations, 1);
229
230 let move_2 = SignedIntent::session(
231 &env,
232 owner.clone(),
233 &key_id,
234 make_action(&env, "move"),
235 1,
236 99999,
237 );
238 let result_2 = kernel.authorize(&env, &move_2).unwrap();
239 assert_eq!(result_2.remaining_operations, 0);
240
241 let replay = SignedIntent::session(
242 &env,
243 owner.clone(),
244 &key_id,
245 make_action(&env, "move"),
246 1,
247 99999,
248 );
249 assert_eq!(
250 kernel.authorize(&env, &replay),
251 Err(AccountError::SessionBudgetExceeded)
252 );
253 });
254 }
255
256 #[test]
257 fn test_session_intent_rejects_wrong_nonce_before_budget_consumption() {
258 let env = Env::default();
259 let contract_id = env.register(TestContract, ());
260 let owner = Address::generate(&env);
261 let key_id = BytesN::from_array(&env, &[8u8; 32]);
262
263 env.as_contract(&contract_id, || {
264 let kernel = AccountKernel::new(owner.clone());
265 let session = SessionKey {
266 key_id: key_id.clone(),
267 scope: SessionScope {
268 allowed_actions: vec![&env, symbol_short!("move")],
269 max_operations: 3,
270 expires_at: 99999,
271 },
272 created_at: 0,
273 operations_used: 0,
274 next_nonce: 2,
275 };
276 SessionStorage::store(&env, &owner, &session);
277
278 let wrong_nonce =
279 SignedIntent::session(&env, owner, &key_id, make_action(&env, "move"), 1, 99999);
280 assert_eq!(
281 kernel.authorize(&env, &wrong_nonce),
282 Err(AccountError::NonceMismatch)
283 );
284 });
285 }
286
287 #[test]
288 fn test_device_and_guardian_policies_share_same_policy_model() {
289 let env = Env::default();
290 let contract_id = env.register(TestContract, ());
291 let owner = Address::generate(&env);
292
293 env.as_contract(&contract_id, || {
294 let mut devices = DeviceManager::new(
295 owner.clone(),
296 DevicePolicy {
297 max_devices: 2,
298 auto_revoke_after: 0,
299 },
300 &env,
301 );
302 let device_key = BytesN::from_array(&env, &[5u8; 32]);
303 devices
304 .register_device(&env, device_key.clone(), symbol_short!("phone"))
305 .unwrap();
306
307 let mut recovery = RecoverableAccount::new(
308 owner.clone(),
309 RecoveryConfig {
310 threshold: 1,
311 timelock_period: 0,
312 max_guardians: 2,
313 },
314 &env,
315 );
316 let guardian = Address::generate(&env);
317 recovery.add_guardian(&env, guardian.clone()).unwrap();
318
319 let device_policy = ActiveDevicePolicy;
320 device_policy
321 .evaluate(
322 &env,
323 &DeviceContext {
324 account: &owner,
325 key_id: &device_key,
326 },
327 )
328 .unwrap();
329
330 let guardian_policy = GuardianPolicy;
331 guardian_policy
332 .evaluate(
333 &env,
334 &RecoveryContext {
335 account: &owner,
336 guardian: &guardian,
337 },
338 )
339 .unwrap();
340 });
341 }
342}