google_wallet/
firebase_wallet.rs

1use base64::{engine::general_purpose, Engine};
2use dioxus_oauth::prelude::FirebaseService;
3use gloo_storage::{errors::StorageError, LocalStorage, Storage};
4use ring::{
5    rand::SystemRandom,
6    signature::{Ed25519KeyPair, KeyPair, Signature},
7};
8use simple_asn1::{
9    oid, to_der,
10    ASN1Block::{BitString, ObjectIdentifier, Sequence},
11};
12
13pub const IDENTITY_KEY: &str = "identity";
14
15#[derive(Debug, Clone)]
16pub struct FirebaseWallet {
17    pub principal: Option<String>,
18    pub firebase: FirebaseService,
19    pub private_key: Option<String>,
20    pub public_key: Option<Vec<u8>>,
21    pub pkcs8: Option<Vec<u8>>,
22
23    pub email: Option<String>,
24    pub name: Option<String>,
25    pub photo_url: Option<String>,
26}
27
28impl FirebaseWallet {
29    pub fn new(
30        api_key: String,
31        auth_domain: String,
32        project_id: String,
33        storage_bucket: String,
34        messaging_sender_id: String,
35        app_id: String,
36        measurement_id: String,
37    ) -> Self {
38        let firebase = FirebaseService::new(
39            api_key,
40            auth_domain,
41            project_id,
42            storage_bucket,
43            messaging_sender_id,
44            app_id,
45            measurement_id,
46        );
47
48        Self {
49            firebase,
50            principal: None,
51            private_key: None,
52            public_key: None,
53            pkcs8: None,
54
55            email: None,
56            name: None,
57            photo_url: None,
58        }
59    }
60
61    pub fn get_login(&self) -> bool {
62        self.principal.is_some()
63    }
64
65    pub fn get_principal(&self) -> String {
66        let public_key = self.public_key.clone().unwrap_or_default();
67
68        let id_ed25519 = oid!(1, 3, 101, 112);
69        let algorithm = Sequence(0, vec![ObjectIdentifier(0, id_ed25519)]);
70        let subject_public_key = BitString(0, public_key.len() * 8, public_key);
71        let subject_public_key_info = Sequence(0, vec![algorithm, subject_public_key]);
72        let der_public_key = to_der(&subject_public_key_info).unwrap();
73        let wallet_address = candid::Principal::self_authenticating(der_public_key);
74        wallet_address.to_text()
75    }
76
77    pub fn get_user_info(&self) -> Option<(String, String, String)> {
78        if self.email.is_none() || self.name.is_none() || self.photo_url.is_none() {
79            return None;
80        }
81
82        Some((
83            self.email.clone().unwrap(),
84            self.name.clone().unwrap(),
85            self.photo_url.clone().unwrap(),
86        ))
87    }
88
89    pub fn try_setup_from_storage(&mut self) -> Option<String> {
90        if self.get_login() {
91            return self.principal.clone();
92        }
93
94        tracing::debug!("try_setup_from_storage");
95        let key: Result<String, StorageError> = LocalStorage::get(IDENTITY_KEY);
96        tracing::debug!("key from storage: {key:?}");
97
98        if let Ok(private_key) = key {
99            tracing::debug!("private_key: {private_key}");
100            self.try_setup_from_private_key(private_key)
101        } else {
102            None
103        }
104    }
105
106    pub fn logout(&mut self) {
107        self.private_key = None;
108        self.public_key = None;
109        self.principal = None;
110        self.email = None;
111        self.name = None;
112        self.photo_url = None;
113
114        let _ = LocalStorage::delete(IDENTITY_KEY);
115    }
116
117    pub async fn request_wallet_with_google(&mut self) -> Result<WalletEvent, String> {
118        use crate::drive_api::DriveApi;
119
120        let cred = self
121            .firebase
122            .sign_in_with_popup(vec![
123                "https://www.googleapis.com/auth/drive.appdata".to_string()
124            ])
125            .await;
126        tracing::debug!("cred: {cred:?}");
127        let cli = DriveApi::new(cred.access_token);
128        let data = match cli.list_files().await {
129            Ok(v) => v,
130            Err(e) => {
131                tracing::error!("failed to get file {e}");
132                return Err(format!("{e:?}"));
133            }
134        };
135        tracing::debug!("data: {data:?}");
136
137        let (evt, pkcs8) = match data
138            .iter()
139            .find(|x| x.name == option_env!("ENV").unwrap_or("local").to_string())
140        {
141            Some(v) => match cli.get_file(&v.id).await {
142                Ok(v) => {
143                    tracing::debug!("file content: {v}");
144
145                    (WalletEvent::Login, v)
146                    // self.try_setup_from_private_key(v);
147
148                    // return Ok(WalletEvent::Login);
149                }
150                Err(e) => {
151                    tracing::warn!("failed to get file {e}");
152
153                    return Err("failed to get file".to_string());
154                }
155            },
156            None => {
157                tracing::warn!("file not found");
158                let rng = SystemRandom::new();
159
160                let key_pair = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
161                let private_key = general_purpose::STANDARD.encode(key_pair.as_ref());
162
163                if let Err(e) = cli.upload_file(&private_key).await {
164                    tracing::error!("failed to upload file {e}");
165                    return Err("failed to upload file".to_string());
166                };
167
168                (WalletEvent::Signup, private_key)
169            }
170        };
171
172        self.try_setup_from_private_key(pkcs8);
173        self.name = Some(cred.display_name);
174        self.email = Some(cred.email);
175        self.photo_url = Some(cred.photo_url);
176
177        Ok(evt)
178    }
179
180    pub fn sign(&self, msg: &str) -> Option<Signature> {
181        tracing::debug!("sign: {msg}");
182        let key_pair = self.get_identity()?;
183
184        Some(key_pair.sign(msg.as_bytes()))
185    }
186
187    pub fn public_key(&self) -> Option<Vec<u8>> {
188        let key_pair = self.get_identity()?;
189
190        Some(key_pair.public_key().as_ref().to_vec())
191    }
192
193    pub fn try_setup_from_private_key(&mut self, private_key: String) -> Option<String> {
194        match general_purpose::STANDARD.decode(&private_key) {
195            Ok(key) => {
196                tracing::debug!("key setup");
197                self.private_key = Some(private_key.clone());
198                if let Some(key_pair) = self.init_or_get_identity(Some(key.as_ref())) {
199                    self.public_key = Some(key_pair.public_key().as_ref().to_vec());
200                    self.principal = Some(self.get_principal());
201                    tracing::debug!("wallet initialized");
202                }
203            }
204            Err(e) => {
205                tracing::error!("Decode Error: {e}");
206
207                return None;
208            }
209        };
210
211        use gloo_storage::Storage;
212        let _ = gloo_storage::LocalStorage::set(IDENTITY_KEY, private_key);
213
214        Some(self.get_principal())
215    }
216
217    pub fn init_or_get_identity(&mut self, pkcs8: Option<&[u8]>) -> Option<Ed25519KeyPair> {
218        if self.pkcs8.is_none() && pkcs8.is_some() {
219            self.pkcs8 = Some(pkcs8.unwrap().to_vec());
220        }
221
222        self.get_identity()
223    }
224
225    pub fn get_identity(&self) -> Option<Ed25519KeyPair> {
226        if let Some(pkcs8) = &self.pkcs8 {
227            let key = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8)
228                .expect("Could not read the key pair.");
229            Some(key)
230        } else {
231            None
232        }
233    }
234}
235
236#[derive(Debug, Clone, Copy)]
237pub enum WalletEvent {
238    Login,
239    Signup,
240    Logout,
241}