1use chrono::{DateTime, Duration, Utc};
2use num_bigint::BigUint;
3use rand::Rng;
4use rsa::RsaPrivateKey;
5
6use crate::crypto::{BlindSigner, BlindUser};
7use crate::error::{EcashError, Result};
8use crate::token::{BlindSignature, BlindedToken, Token, TokenMetadata};
9
10pub struct Institution {
11 signer: BlindSigner,
12 institution_id: String,
13 key_id: String,
14 denominations: Vec<u64>,
15 default_expiry: Duration,
16}
17
18impl Institution {
19 pub fn new(
20 private_key: RsaPrivateKey,
21 institution_id: String,
22 key_id: String,
23 denominations: Vec<u64>,
24 default_expiry_days: i64,
25 ) -> Self {
26 Self {
27 signer: BlindSigner::from_keys(private_key),
28 institution_id,
29 key_id,
30 denominations,
31 default_expiry: Duration::days(default_expiry_days),
32 }
33 }
34
35 pub fn institution_id(&self) -> &str {
36 &self.institution_id
37 }
38
39 pub fn public_key(&self) -> &rsa::RsaPublicKey {
40 self.signer.public_key()
41 }
42
43 pub fn validate_denomination(&self, denomination: u64) -> Result<()> {
44 if self.denominations.contains(&denomination) {
45 Ok(())
46 } else {
47 Err(EcashError::InvalidDenomination)
48 }
49 }
50
51 pub fn sign_blinded_token(&self, blinded: &BlindedToken) -> Result<BlindSignature> {
52 self.validate_denomination(blinded.denomination)?;
53
54 let blinded_msg = BigUint::from_bytes_be(&blinded.blinded_message);
55 let signature = self.signer.sign_blinded(&blinded_msg)?;
56
57 Ok(BlindSignature {
58 signature: signature.to_bytes_be(),
59 key_id: self.key_id.clone(),
60 })
61 }
62
63 pub fn verify_token(&self, token: &Token) -> Result<bool> {
64 if token.is_expired() {
65 return Ok(false);
66 }
67
68 self.validate_denomination(token.denomination)?;
69
70 let user = BlindUser::new(self.signer.public_key().clone());
71 let message = Self::construct_message(
72 &token.serial_number,
73 token.denomination,
74 &token.currency,
75 &token.issued_at,
76 );
77
78 let signature = BigUint::from_bytes_be(&token.signature);
79 Ok(user.verify_signature(&message, &signature))
80 }
81
82 pub fn expiry_time(&self) -> DateTime<Utc> {
83 Utc::now() + self.default_expiry
84 }
85
86 fn construct_message(
87 serial: &[u8],
88 denomination: u64,
89 currency: &str,
90 issued_at: &DateTime<Utc>,
91 ) -> Vec<u8> {
92 let mut message = Vec::new();
93 message.extend_from_slice(serial);
94 message.extend_from_slice(&denomination.to_be_bytes());
95 message.extend_from_slice(currency.as_bytes());
96 message.extend_from_slice(&issued_at.timestamp().to_be_bytes());
97 message
98 }
99}
100
101pub struct Wallet {
102 user: BlindUser,
103 institution_id: String,
104 currency: String,
105}
106
107impl Wallet {
108 pub fn new(public_key: rsa::RsaPublicKey, institution_id: String, currency: String) -> Self {
109 Self {
110 user: BlindUser::new(public_key),
111 institution_id,
112 currency,
113 }
114 }
115
116 pub fn prepare_withdrawal(
117 &self,
118 amount: u64,
119 denomination: u64,
120 ) -> Result<Vec<(BlindedToken, TokenMetadata)>> {
121 let count = amount.div_ceil(denomination);
122 let mut tokens = Vec::new();
123
124 for _ in 0..count {
125 let serial = Self::generate_serial();
126 let message = Self::construct_message_for_serial(&serial, denomination, &self.currency);
127
128 let (blinded, blinding_factor) = self.user.blind_message(&message)?;
129
130 tokens.push((
131 BlindedToken {
132 blinded_message: blinded.to_bytes_be(),
133 denomination,
134 currency: self.currency.clone(),
135 },
136 TokenMetadata {
137 serial_number: serial,
138 blinding_factor: blinding_factor.to_bytes_be(),
139 denomination,
140 currency: self.currency.clone(),
141 },
142 ));
143 }
144
145 Ok(tokens)
146 }
147
148 pub fn finalize_withdrawal(
149 &self,
150 blind_signatures: Vec<BlindSignature>,
151 metadata: Vec<TokenMetadata>,
152 expires_at: DateTime<Utc>,
153 ) -> Result<Vec<Token>> {
154 if blind_signatures.len() != metadata.len() {
155 return Err(EcashError::CryptoError);
156 }
157
158 let mut tokens = Vec::new();
159
160 for (blind_sig, meta) in blind_signatures.into_iter().zip(metadata) {
161 let blind_signature = BigUint::from_bytes_be(&blind_sig.signature);
162 let blinding_factor = BigUint::from_bytes_be(&meta.blinding_factor);
163
164 let signature = self
165 .user
166 .unblind_signature(&blind_signature, &blinding_factor)?;
167
168 let message = Self::construct_message_for_serial(
169 &meta.serial_number,
170 meta.denomination,
171 &meta.currency,
172 );
173
174 if !self.user.verify_signature(&message, &signature) {
175 return Err(EcashError::InvalidSignature);
176 }
177
178 tokens.push(Token::new(
179 meta.serial_number,
180 meta.denomination,
181 meta.currency,
182 signature.to_bytes_be(),
183 expires_at,
184 self.institution_id.clone(),
185 blind_sig.key_id,
186 ));
187 }
188
189 Ok(tokens)
190 }
191
192 fn generate_serial() -> Vec<u8> {
193 let mut rng = rand::thread_rng();
194 let mut serial = vec![0u8; 32];
195 rng.fill(&mut serial[..]);
196 serial
197 }
198
199 fn construct_message_for_serial(serial: &[u8], denomination: u64, currency: &str) -> Vec<u8> {
200 let mut message = Vec::new();
201 message.extend_from_slice(serial);
202 message.extend_from_slice(&denomination.to_be_bytes());
203 message.extend_from_slice(currency.as_bytes());
204 message.extend_from_slice(&Utc::now().timestamp().to_be_bytes());
205 message
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use rand::thread_rng;
213 use rsa::RsaPrivateKey;
214
215 #[test]
216 fn test_full_withdrawal_flow() {
217 let mut rng = thread_rng();
218 let private_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
219 let public_key = private_key.to_public_key();
220
221 let institution = Institution::new(
222 private_key,
223 "inst_test".to_string(),
224 "key_001".to_string(),
225 vec![10, 50, 100],
226 90,
227 );
228
229 let wallet = Wallet::new(public_key, "inst_test".to_string(), "USD".to_string());
230
231 let tokens_to_prepare = wallet.prepare_withdrawal(100, 50).unwrap();
232 let (blinded_tokens, metadata): (Vec<_>, Vec<_>) = tokens_to_prepare.into_iter().unzip();
233
234 let blind_signatures: Vec<_> = blinded_tokens
235 .iter()
236 .map(|bt| institution.sign_blinded_token(bt).unwrap())
237 .collect();
238
239 let tokens = wallet
240 .finalize_withdrawal(blind_signatures, metadata, institution.expiry_time())
241 .unwrap();
242
243 for token in &tokens {
244 assert!(institution.verify_token(token).unwrap());
245 }
246 }
247}