setup.rs (6188B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::io::ErrorKind; 18 19 use p256::ecdsa::SigningKey; 20 use taler_common::{json_file, types::base32::Base32}; 21 use tracing::{info, warn}; 22 23 use crate::magnet_api::{api::ErrKind, client::AuthClient, oauth::Token}; 24 use crate::{ 25 config::WorkerCfg, 26 magnet_api::{api::MagnetError, oauth::TokenAuth}, 27 }; 28 29 #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] 30 struct KeysFile { 31 access_token: Option<Token>, 32 signing_key: Option<Base32<32>>, 33 } 34 35 #[derive(Debug)] 36 pub struct Keys { 37 pub access_token: Token, 38 pub signing_key: SigningKey, 39 } 40 41 pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { 42 // Load JSON file 43 let file: KeysFile = match json_file::load(&cfg.keys_path) { 44 Ok(file) => file, 45 Err(e) => { 46 return Err(anyhow::anyhow!( 47 "Could not read magnet keys at '{}': {}", 48 cfg.keys_path, 49 e.kind() 50 )); 51 } 52 }; 53 54 fn incomplete_err() -> anyhow::Error { 55 anyhow::anyhow!("Missing magnet keys, run 'taler-magnet-bank setup' first") 56 } 57 58 // Check full 59 let access_token = file.access_token.ok_or_else(incomplete_err)?; 60 let signing_key = file.signing_key.ok_or_else(incomplete_err)?; 61 62 // Load signing key 63 64 let signing_key = SigningKey::from_slice(&*signing_key)?; 65 66 Ok(Keys { 67 access_token, 68 signing_key, 69 }) 70 } 71 72 pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> { 73 if reset 74 && let Err(e) = std::fs::remove_file(&cfg.keys_path) 75 && e.kind() != ErrorKind::NotFound 76 { 77 Err(e)?; 78 } 79 let mut keys = match json_file::load(&cfg.keys_path) { 80 Ok(existing) => existing, 81 Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(), 82 Err(e) => Err(e)?, 83 }; 84 let client = reqwest::Client::new(); 85 let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer); 86 87 info!("Setup OAuth access token"); 88 if keys.access_token.is_none() { 89 let token_request = client.token_request().await?; 90 91 // TODO how to do it in a generic way ? 92 // TODO Ask MagnetBank if they could support out-of-band configuration 93 println!( 94 "Login at {}?oauth_token={}", 95 client 96 .api_url 97 .join("/NetBankOAuth/authtoken.xhtml") 98 .unwrap(), 99 token_request.key 100 ); 101 let auth_url = rpassword::prompt_password("Enter the result URL>")?; 102 let auth_url = reqwest::Url::parse(&auth_url)?; 103 let token_auth: TokenAuth = 104 serde_urlencoded::from_str(auth_url.query().unwrap_or_default())?; 105 assert_eq!(token_request.key, token_auth.oauth_token); 106 107 let access_token = client.token_access(&token_request, &token_auth).await?; 108 keys.access_token = Some(access_token); 109 json_file::persist(&cfg.keys_path, &keys)?; 110 } 111 112 let client = client.upgrade(keys.access_token.as_ref().unwrap()); 113 114 info!("Setup Strong Customer Authentication"); 115 // TODO find a proper way to check if SCA is required without triggering SCA.GLOBAL_FEATURE_NOT_ENABLED 116 let request = client.request_sms_code().await?; 117 println!( 118 "A SCA code have been sent through {} to {}", 119 request.channel, 120 request.sent_to.join(", ") 121 ); 122 let sca_code = rpassword::prompt_password("Enter the code>")?; 123 if let Err(e) = client.perform_sca(&sca_code).await { 124 // Ignore error if SCA already performed 125 if !matches!(e.kind, ErrKind::Magnet(MagnetError { ref short_message, .. }) if short_message == "TOKEN_SCA_HITELESITETT") 126 { 127 return Err(e.into()); 128 } 129 } 130 131 info!("Setup public key"); 132 // TODO find a proper way to check if a public key have been setup 133 // TODO use the better from/to_array API in the next version of the crypto lib 134 let signing_key = match keys.signing_key { 135 Some(bytes) => SigningKey::from_slice(bytes.as_ref())?, 136 None => { 137 let rand = SigningKey::random(&mut rand_core::OsRng); 138 let array: [u8; 32] = rand.to_bytes().into(); 139 keys.signing_key = Some(Base32::from(array)); 140 json_file::persist(&cfg.keys_path, &keys)?; 141 rand 142 } 143 }; 144 if let Err(e) = client.upload_public_key(&signing_key).await { 145 // Ignore error if public key already uploaded 146 if !matches!(e.kind, ErrKind::Magnet(MagnetError { ref short_message, .. }) if short_message== "KULCS_MAR_HASZNALATBAN") 147 { 148 return Err(e.into()); 149 } 150 } 151 152 info!("Check account"); 153 let res = client.list_accounts().await?; 154 let mut ibans = Vec::new(); 155 for partner in res.partners { 156 for account in partner.bank_accounts { 157 if *cfg.payto == account.iban { 158 if partner.partner.name != cfg.payto.name { 159 warn!( 160 "Expected name '{}' from config got '{}' from bank", 161 cfg.payto.name, partner.partner.name 162 ); 163 } 164 return Ok(()); 165 } else { 166 ibans.push(account.iban); 167 } 168 } 169 } 170 Err(anyhow::anyhow!( 171 "Unknown account {} from config, expected one of the following account {}", 172 cfg.payto.0, 173 ibans 174 .into_iter() 175 .map(|it| it.to_string()) 176 .collect::<Vec<_>>() 177 .join(", ") 178 )) 179 }