ecash_core/
protocol.rs

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}