libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }