setup.rs (8501B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 2026 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 aws_lc_rs::{encoding::AsBigEndian, signature::EcdsaKeyPair}; 20 use taler_common::{json_file, types::base32::Base32}; 21 use tracing::{info, warn}; 22 23 use crate::{ 24 config::WorkerCfg, 25 magnet_api::{api::MagnetError, oauth::TokenAuth}, 26 }; 27 use crate::{ 28 constants::MAGNET_SIGNATURE, 29 magnet_api::{api::MagnetErr, client::AuthClient, oauth::Token}, 30 }; 31 32 #[derive(Default, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] 33 struct KeysFile { 34 access_token: Option<Token>, 35 signing_key: Option<Base32<32>>, 36 } 37 38 #[derive(Debug)] 39 pub struct Keys { 40 pub access_token: Token, 41 pub signing_key: EcdsaKeyPair, 42 } 43 44 pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { 45 // Load JSON file 46 let file: KeysFile = match json_file::load(&cfg.keys_path) { 47 Ok(file) => file, 48 Err(e) => { 49 return Err(anyhow::anyhow!( 50 "Could not read magnet keys at '{}': {}", 51 cfg.keys_path, 52 e.kind() 53 )); 54 } 55 }; 56 57 fn incomplete_err() -> anyhow::Error { 58 anyhow::anyhow!("Missing magnet keys, run 'taler-magnet-bank setup' first") 59 } 60 61 // Check full 62 let access_token = file.access_token.ok_or_else(incomplete_err)?; 63 let signing_key = file.signing_key.ok_or_else(incomplete_err)?; 64 65 // Load signing key 66 let signing_key = parse_private_key(&signing_key)?; 67 68 Ok(Keys { 69 access_token, 70 signing_key, 71 }) 72 } 73 74 pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> { 75 if reset 76 && let Err(e) = std::fs::remove_file(&cfg.keys_path) 77 && e.kind() != ErrorKind::NotFound 78 { 79 Err(e)?; 80 } 81 let mut keys = match json_file::load(&cfg.keys_path) { 82 Ok(existing) => existing, 83 Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(), 84 Err(e) => Err(e)?, 85 }; 86 let client = http_client::client()?; 87 let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer); 88 89 info!("Setup OAuth access token"); 90 if keys.access_token.is_none() { 91 let token_request = client.token_request().await?; 92 93 // TODO how to do it in a generic way ? 94 // TODO Ask MagnetBank if they could support out-of-band configuration 95 println!( 96 "Login at {}?oauth_token={}", 97 client 98 .api_url 99 .join("/NetBankOAuth/authtoken.xhtml") 100 .unwrap(), 101 token_request.key 102 ); 103 let auth_url = rpassword::prompt_password("Enter the result URL>")?; 104 let auth_url = url::Url::parse(&auth_url)?; 105 let token_auth: TokenAuth = 106 serde_urlencoded::from_str(auth_url.query().unwrap_or_default())?; 107 assert_eq!(token_request.key, token_auth.oauth_token); 108 109 let access_token = client.token_access(&token_request, &token_auth).await?; 110 keys.access_token = Some(access_token); 111 json_file::persist(&cfg.keys_path, &keys)?; 112 } 113 114 let client = client.upgrade(keys.access_token.as_ref().unwrap()); 115 116 info!("Setup Strong Customer Authentication"); 117 // TODO find a proper way to check if SCA is required without triggering SCA.GLOBAL_FEATURE_NOT_ENABLED 118 let request = client.request_sms_code().await?; 119 println!( 120 "A SCA code have been sent through {} to {}", 121 request.channel, 122 request.sent_to.join(", ") 123 ); 124 let sca_code = rpassword::prompt_password("Enter the code>")?; 125 if let Err(e) = client.perform_sca(&sca_code).await { 126 // Ignore error if SCA already performed 127 if !matches!(e.err, MagnetErr::Magnet(MagnetError { ref short_message, .. }) if short_message == "TOKEN_SCA_HITELESITETT") 128 { 129 return Err(e.into()); 130 } 131 } 132 133 info!("Setup public key"); 134 // TODO find a proper way to check if a public key have been setup 135 let signing_key = match keys.signing_key { 136 Some(bytes) => parse_private_key(&bytes)?, 137 None => { 138 let rand = EcdsaKeyPair::generate(MAGNET_SIGNATURE)?; 139 keys.signing_key = Some(Base32::from(encode_private_key(&rand)?)); 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.err, MagnetErr::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 } 180 181 /** Parse a 32B ECDSA private key */ 182 fn parse_private_key(encoded: &[u8; 32]) -> anyhow::Result<EcdsaKeyPair> { 183 // Recreate the pkcs8 from the raw private key bytes as aws-lc-rs does not support the raw bytes 184 let mut pkcs8 = [ 185 // --- PKCS#8 Header --- 186 0x30, 0x41, // Sequence (65 bytes remaining) 187 0x02, 0x01, 0x00, // Version v1 (0) 188 // --- AlgorithmIdentifier (P-256) --- 189 0x30, 0x13, // Sequence (19 bytes) 190 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID: ecPublicKey 191 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID: prime256v1 192 // --- PrivateKey (Wrapped Octet String) --- 193 0x04, 0x27, // Octet String (39 bytes) 194 // --- Inside: The ECPrivateKey Structure (RFC 5915) --- 195 0x30, 0x25, // Sequence (37 bytes) 196 0x02, 0x01, 0x01, // Version 1 197 0x04, 0x20, // Octet String (32 bytes) 198 // [32 bytes of key data will go here] 199 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200 0, 201 ]; 202 pkcs8[35..67].copy_from_slice(encoded); 203 204 let key = EcdsaKeyPair::from_pkcs8(MAGNET_SIGNATURE, &pkcs8)?; 205 Ok(key) 206 } 207 208 /** Encode a ECDSA private key into 32B */ 209 fn encode_private_key(key: &EcdsaKeyPair) -> anyhow::Result<[u8; 32]> { 210 let array: [u8; 32] = key.private_key().as_be_bytes()?.as_ref().try_into()?; 211 Ok(array) 212 } 213 214 #[cfg(test)] 215 mod test { 216 use taler_common::json_file; 217 218 use crate::setup::{KeysFile, encode_private_key, parse_private_key}; 219 220 #[test] 221 fn keys_files() { 222 // Load JSON file 223 let content: KeysFile = json_file::load("tests/fixtures/setup.json").unwrap(); 224 // Check full 225 assert!(content.access_token.is_some()); 226 let key = content.signing_key.clone().unwrap(); 227 228 // Load signing key 229 let secret_key = parse_private_key(&key).unwrap(); 230 231 // Check encoded round trip 232 assert_eq!(encode_private_key(&secret_key).unwrap(), *key); 233 234 // Check JSON round trip 235 let tmp_path = "/tmp/keys.json"; 236 if std::fs::exists(tmp_path).unwrap() { 237 std::fs::remove_file(tmp_path).unwrap(); 238 } 239 json_file::persist(tmp_path, &content).unwrap(); 240 assert_eq!(json_file::load::<KeysFile>(tmp_path).unwrap(), content); 241 } 242 }