google_wallet/
firebase_wallet.rs1use 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 }
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}