setup.kt (9699B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2025 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.ebics 21 22 import io.ktor.client.HttpClient 23 import tech.libeufin.common.crypto.CryptoUtil 24 import tech.libeufin.common.encodeUpHex 25 import tech.libeufin.common.fmtChunkByTwo 26 import tech.libeufin.ebics.EbicsKeyMng.Order.* 27 import java.time.Instant 28 import kotlin.io.path.Path 29 import kotlin.io.path.writeBytes 30 import java.nio.file.FileAlreadyExistsException 31 import java.nio.file.Path 32 import java.nio.file.StandardOpenOption 33 34 /** Load client private keys at [path] or create new ones if missing */ 35 private fun loadOrGenerateClientKeys(path: Path): ClientPrivateKeysFile { 36 // If exists load from disk 37 val current = loadClientKeys(path) 38 if (current != null) return current 39 // Else create new keys 40 val newKeys = generateNewKeys() 41 persistClientKeys(newKeys, path) 42 logger.info("New client private keys created at '$path'") 43 return newKeys 44 } 45 46 /** 47 * Asks the user to accept the bank public keys. 48 * 49 * @param bankKeys bank public keys, in format stored on disk. 50 * @return true if the user accepted, false otherwise. 51 */ 52 fun askUserToAcceptKeys(bankKeys: BankPublicKeysFile, cfg: EbicsSetupConfig): Boolean { 53 val encHash = CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key) 54 val authHash = CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key) 55 val authPubKey = cfg.bankAuthPubKey 56 val encPubKey = cfg.bankEncPubKey 57 if (authPubKey != null && encPubKey != null) { 58 if (encHash.contentEquals(encPubKey) && authHash.contentEquals(authPubKey)) { 59 logger.info("Accepting bank keys matching config hashes") 60 return true 61 } 62 throw Exception(buildString { 63 append("Bank keys does not match config hashes\nBank encryption key: ") 64 append(encHash.encodeUpHex().fmtChunkByTwo()) 65 append("\nConfig encryption key: ") 66 append(encPubKey.encodeUpHex().fmtChunkByTwo()) 67 append("\nBank authentication key: ") 68 append(authHash.encodeUpHex().fmtChunkByTwo()) 69 append("\nConfig authentication key: ") 70 append(authPubKey.encodeUpHex().fmtChunkByTwo()) 71 }) 72 } 73 println("The bank has the following keys:") 74 println("Encryption key: ${encHash.encodeUpHex().fmtChunkByTwo()}") 75 println("Authentication key: ${authHash.encodeUpHex().fmtChunkByTwo()}") 76 print("type 'yes, accept' to accept them: ") 77 val userResponse: String? = readlnOrNull() 78 return userResponse == "yes, accept" 79 } 80 81 82 /** 83 * Mere collector of the PDF generation steps. Fails the 84 * process if a problem occurs. 85 * 86 * @param privs client private keys. 87 * @param cfg configuration handle. 88 */ 89 private fun makePdf(privs: ClientPrivateKeysFile, cfg: EbicsHostConfig) { 90 val pdf = generateKeysPdf(privs, cfg) 91 val path = Path("/tmp/libeufin-ebics-keys.pdf") 92 try { 93 path.writeBytes(pdf, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) 94 } catch (e: Exception) { 95 throw Exception("Could not write PDF to '$path'", e) 96 } 97 println("PDF file with keys created at '$path'") 98 } 99 100 101 /** Perform an EBICS public key management [order] using [client] and update on disk state */ 102 private suspend fun submitClientKeys( 103 keyCfg: EbicsKeysConfig, 104 hostCfg: EbicsHostConfig, 105 privs: ClientPrivateKeysFile, 106 client: HttpClient, 107 ebicsLogger: EbicsLogger, 108 order: EbicsKeyMng.Order, 109 ebics3: Boolean 110 ) { 111 require(order != HPB) { "Only INI & HIA are supported for client keys" } 112 val resp = keyManagement(hostCfg, privs, client, ebicsLogger, order, ebics3) 113 if (resp.technicalCode == EbicsReturnCode.EBICS_INVALID_USER_OR_USER_STATE || resp.technicalCode == EbicsReturnCode.EBICS_INVALID_USER_STATE) { 114 throw Exception("$order status code ${resp.technicalCode}: either your IDs are incorrect, or you already have keys registered with this bank") 115 } 116 val orderData = resp.okOrFail(order.name) 117 when (order) { 118 INI -> privs.submitted_ini = true 119 HIA -> privs.submitted_hia = true 120 HPB -> {} 121 } 122 try { 123 persistClientKeys(privs, keyCfg.clientPrivateKeysPath) 124 } catch (e: Exception) { 125 throw Exception("Could not update the $order state on disk", e) 126 } 127 } 128 129 /** Perform an EBICS private key management HPB using [client] */ 130 private suspend fun fetchPrivateKeys( 131 cfg: EbicsHostConfig, 132 privs: ClientPrivateKeysFile, 133 client: HttpClient, 134 ebicsLogger: EbicsLogger, 135 ebics3: Boolean 136 ): BankPublicKeysFile { 137 val order = HPB 138 val resp = keyManagement(cfg, privs, client, ebicsLogger, order, ebics3) 139 if (resp.technicalCode == EbicsReturnCode.EBICS_AUTHENTICATION_FAILED) { 140 throw Exception("$order status code ${resp.technicalCode}: could not download bank keys, send client keys (and/or related PDF document with --generate-registration-pdf) to the bank") 141 } 142 val orderData = requireNotNull(resp.okOrFail(order.name)) { 143 "$order: missing order data" 144 } 145 val (authPub, encPub) = EbicsKeyMng.parseHpbOrder(orderData) 146 return BankPublicKeysFile( 147 bank_authentication_public_key = authPub, 148 bank_encryption_public_key = encPub, 149 accepted = false 150 ) 151 } 152 153 154 suspend fun ebicsSetup( 155 client: HttpClient, 156 ebicsLogger: EbicsLogger, 157 keyCfg: EbicsKeysConfig, 158 hostCfg: EbicsHostConfig, 159 setupCfg: EbicsSetupConfig, 160 forceKeysResubmission: Boolean, 161 generateRegistrationPdf: Boolean, 162 autoAcceptKeys: Boolean, 163 ebics3: Boolean 164 ): Pair<ClientPrivateKeysFile, BankPublicKeysFile>{ 165 val clientKeys = loadOrGenerateClientKeys(keyCfg.clientPrivateKeysPath) 166 var bankKeys = loadBankKeys(keyCfg.bankPublicKeysPath) 167 168 // Check EBICS 3 support 169 val versions = HEV(client, hostCfg, ebicsLogger) 170 logger.debug("HEV: {}", versions) 171 if (!versions.contains(VersionNumber(3.0f, "H005")) && !versions.contains(VersionNumber(3.02f, "H005"))) { 172 throw Exception("EBICS 3 is not supported by your bank") 173 } 174 175 // Privs exist. Upload their pubs 176 val keysNotSub = !clientKeys.submitted_ini 177 if (!clientKeys.submitted_ini || forceKeysResubmission) 178 submitClientKeys(keyCfg, hostCfg, clientKeys, client, ebicsLogger, INI, ebics3) 179 // Eject PDF if the keys were submitted for the first time, or the user asked. 180 if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, hostCfg) 181 if (!clientKeys.submitted_hia || forceKeysResubmission) 182 submitClientKeys(keyCfg, hostCfg, clientKeys, client, ebicsLogger, HIA, ebics3) 183 184 val fetchedBankKeys = fetchPrivateKeys(hostCfg, clientKeys, client, ebicsLogger, ebics3) 185 if (bankKeys == null) { 186 // Accept bank keys 187 logger.info("Bank keys stored at ${keyCfg.bankPublicKeysPath}") 188 try { 189 persistBankKeys(fetchedBankKeys, keyCfg.bankPublicKeysPath) 190 } catch (e: Exception) { 191 throw Exception("Could not store bank keys on disk", e) 192 } 193 bankKeys = fetchedBankKeys 194 } else { 195 // Check current bank keys 196 if (bankKeys.bank_encryption_public_key != fetchedBankKeys.bank_encryption_public_key) { 197 throw Exception(buildString { 198 append("On disk bank encryption key stored at ") 199 append(keyCfg.bankPublicKeysPath) 200 append(" doesn't match server key\nDisk: ") 201 append(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeUpHex().fmtChunkByTwo()) 202 append("\nServer: ") 203 append(CryptoUtil.getEbicsPublicKeyHash(fetchedBankKeys.bank_encryption_public_key).encodeUpHex().fmtChunkByTwo()) 204 }) 205 } else if (bankKeys.bank_authentication_public_key != fetchedBankKeys.bank_authentication_public_key) { 206 throw Exception(buildString { 207 append("On disk bank authentication key stored at ") 208 append(keyCfg.bankPublicKeysPath) 209 append(" doesn't match server key\nDisk: ") 210 append(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeUpHex().fmtChunkByTwo()) 211 append("\nServer: ") 212 append(CryptoUtil.getEbicsPublicKeyHash(fetchedBankKeys.bank_authentication_public_key).encodeUpHex().fmtChunkByTwo()) 213 }) 214 } 215 } 216 217 if (!bankKeys.accepted) { 218 // Finishing the setup by accepting the bank keys. 219 if (autoAcceptKeys) bankKeys.accepted = true 220 else bankKeys.accepted = askUserToAcceptKeys(bankKeys, setupCfg) 221 222 if (!bankKeys.accepted) { 223 throw Exception("Cannot successfully finish the setup without accepting the bank keys") 224 } 225 try { 226 persistBankKeys(bankKeys, keyCfg.bankPublicKeysPath) 227 } catch (e: Exception) { 228 throw Exception("Could not set bank keys as accepted on disk", e) 229 } 230 } 231 232 return Pair(clientKeys, bankKeys) 233 }