taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

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 }