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