Java tutorial
/** * Axelor Business Solutions * * Copyright (C) 2016 Axelor (<http://axelor.com>). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.axelor.apps.account.service.payment; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileReader; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.List; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.codec.Base64; import org.bouncycastle.util.io.pem.PemReader; import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.axelor.apps.account.db.AccountingSituation; import com.axelor.apps.account.db.PayboxConfig; import com.axelor.apps.account.db.PaymentVoucher; import com.axelor.apps.account.exception.IExceptionMessage; import com.axelor.apps.account.service.AccountingSituationService; import com.axelor.apps.account.service.config.PayboxConfigService; import com.axelor.apps.base.db.Company; import com.axelor.apps.base.db.Partner; import com.axelor.apps.base.db.repo.PartnerRepository; import com.axelor.apps.base.service.PartnerService; import com.axelor.apps.base.service.administration.GeneralServiceImpl; import com.axelor.apps.tool.StringTool; import com.axelor.exception.AxelorException; import com.axelor.exception.db.IException; import com.axelor.i18n.I18n; import com.axelor.inject.Beans; import com.google.inject.persist.Transactional; public class PayboxService { private final Logger log = LoggerFactory.getLogger(getClass()); protected PayboxConfigService payboxConfigService; protected PartnerService partnerService; protected PartnerRepository partnerRepository; protected final String CHARSET = "UTF-8"; protected final String HASH_ENCRYPTION_ALGORITHM = "SHA1withRSA"; protected final String ENCRYPTION_ALGORITHM = "RSA"; public PayboxService(PayboxConfigService payboxConfigService, PartnerService partnerService, PartnerRepository partnerRepository) { this.payboxConfigService = payboxConfigService; this.partnerService = partnerService; this.partnerRepository = partnerRepository; } /** * Procdure permettant de raliser un paiement avec Paybox * @param paymentVoucher * Une saisie paiement * @throws AxelorException * @throws UnsupportedEncodingException */ public String paybox(PaymentVoucher paymentVoucher) throws AxelorException, UnsupportedEncodingException { this.checkPayboxPaymentVoucherFields(paymentVoucher); Company company = paymentVoucher.getCompany(); BigDecimal paidAmount = paymentVoucher.getPaidAmount(); Partner payerPartner = paymentVoucher.getPartner(); // this.checkPayboxPartnerFields(payerPartner); this.checkPaidAmount(payerPartner, company, paidAmount); this.checkPaidAmount(paymentVoucher); PayboxConfig payboxConfig = payboxConfigService.getPayboxConfig(company); // Vrification du remplissage du chemin de la cl publique Paybox payboxConfigService.getPayboxPublicKeyPath(payboxConfig); String payboxUrl = payboxConfigService.getPayboxUrl(payboxConfig); String pbxSite = payboxConfigService.getPayboxSite(payboxConfig); String pbxRang = payboxConfigService.getPayboxRang(payboxConfig); String pbxDevise = payboxConfigService.getPayboxDevise(payboxConfig); String pbxTotal = paidAmount.setScale(2).toString().replace(".", ""); String pbxCmd = paymentVoucher.getRef(); // Identifiant de la saisie paiement String pbxPorteur = this.getPartnerEmail(paymentVoucher); String pbxRetour = payboxConfigService.getPayboxRetour(payboxConfig); // String pbxEffectue = this.encodeUrl(this.replaceVariableInUrl(accountConfigService.getPayboxRetourUrlEffectue(accountConfig), paymentVoucher)); String pbxEffectue = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlEffectue(payboxConfig), paymentVoucher); String pbxRefuse = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlRefuse(payboxConfig), paymentVoucher); String pbxAnnule = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlAnnule(payboxConfig), paymentVoucher); String pbxIdentifiant = payboxConfigService.getPayboxIdentifiant(payboxConfig); String pbxHash = payboxConfigService.getPayboxHashSelect(payboxConfig); String pbxHmac = payboxConfigService.getPayboxHmac(payboxConfig); //Date laquelle l'empreinte HMAC a t calcule (format ISO8601) String pbxTime = ISODateTimeFormat.dateHourMinuteSecond().print(new DateTime()); // Permet de restreindre les modes de paiement String pbxTypepaiement = "CARTE"; String pbxTypecarte = "CB"; String message = this.buildMessage(pbxSite, pbxRang, pbxIdentifiant, pbxTotal, pbxDevise, pbxCmd, pbxPorteur, pbxRetour, pbxEffectue, pbxRefuse, pbxAnnule, pbxHash, pbxTime, pbxTypepaiement, pbxTypecarte); log.debug("Message : {}", message); String messageHmac = this.getHmacSignature(message, pbxHmac, pbxHash); log.debug("Message HMAC : {}", messageHmac); String messageEncode = this.buildMessage(URLEncoder.encode(pbxSite, this.CHARSET), URLEncoder.encode(pbxRang, this.CHARSET), URLEncoder.encode(pbxIdentifiant, this.CHARSET), pbxTotal, URLEncoder.encode(pbxDevise, this.CHARSET), URLEncoder.encode(pbxCmd, this.CHARSET), URLEncoder.encode(pbxPorteur, this.CHARSET), URLEncoder.encode(pbxRetour, this.CHARSET), URLEncoder.encode(pbxEffectue, this.CHARSET), URLEncoder.encode(pbxRefuse, this.CHARSET), URLEncoder.encode(pbxAnnule, this.CHARSET), URLEncoder.encode(pbxHash, this.CHARSET), URLEncoder.encode(pbxTime, this.CHARSET), URLEncoder.encode(pbxTypepaiement, this.CHARSET), URLEncoder.encode(pbxTypecarte, this.CHARSET)); String url = payboxUrl + messageEncode + "&PBX_HMAC=" + messageHmac; log.debug("Url : {}", url); return url; } public String buildMessage(String pbxSite, String pbxRang, String pbxIdentifiant, String pbxTotal, String pbxDevise, String pbxCmd, String pbxPorteur, String pbxRetour, String pbxEffectue, String pbxRefuse, String pbxAnnule, String pbxHash, String pbxTime, String pbxTypepaiement, String pbxTypecarte) { return String.format("PBX_SITE=%s&PBX_RANG=%s&PBX_IDENTIFIANT=%s&PBX_TOTAL=%s&PBX_DEVISE=%s" + "&PBX_CMD=%s&PBX_PORTEUR=%s&PBX_RETOUR=%s&PBX_EFFECTUE=%s&PBX_REFUSE=%s&PBX_ANNULE=%s&PBX_HASH=%s&PBX_TIME=%s&PBX_TYPEPAIEMENT=%s&PBX_TYPECARTE=%s", pbxSite, pbxRang, pbxIdentifiant, pbxTotal, pbxDevise, pbxCmd, pbxPorteur, pbxRetour, pbxEffectue, pbxRefuse, pbxAnnule, pbxHash, pbxTime, pbxTypepaiement, pbxTypecarte); } /** * Fonction remplaant le paramtre %id par le numro d'id de la saisie paiement * @param url * @param paymentVoucher * @return */ public String replaceVariableInUrl(String url, PaymentVoucher paymentVoucher) { return url.replaceAll("%idPV", paymentVoucher.getId().toString()); } /** * Fonction convertissant l'url en url encod * @param url * @return */ public String encodeUrl(String url) { String newUrl = url.replaceAll("\\%", "%25"); newUrl = newUrl.replaceAll("\\?", "%3F"); newUrl = newUrl.replaceAll("\\/", "%2F"); newUrl = newUrl.replaceAll("\\:", "%3A"); newUrl = newUrl.replaceAll("\\#", "%23"); newUrl = newUrl.replaceAll("\\&", "%26"); newUrl = newUrl.replaceAll("\\=", "%3D"); newUrl = newUrl.replaceAll("\\+", "%2B"); newUrl = newUrl.replaceAll("\\$", "%24"); newUrl = newUrl.replaceAll("\\,", "%2C"); newUrl = newUrl.replaceAll(" ", "%20"); newUrl = newUrl.replaceAll("\\;", "%3B"); newUrl = newUrl.replaceAll("\\<", "%3C"); newUrl = newUrl.replaceAll("\\>", "%3E"); newUrl = newUrl.replaceAll("\\~", "%7E"); newUrl = newUrl.replaceAll("\\.", "%2E"); return newUrl; // return url; } public String getPartnerEmail(PaymentVoucher paymentVoucher) throws AxelorException { Partner partner = paymentVoucher.getPartner(); Company company = paymentVoucher.getCompany(); if (partner.getEmailAddress().getAddress() != null && !partner.getEmailAddress().getAddress().isEmpty()) { return partner.getEmailAddress().getAddress(); } else if (paymentVoucher.getEmail() != null && !paymentVoucher.getEmail().isEmpty()) { return paymentVoucher.getEmail(); } else { return payboxConfigService.getPayboxDefaultEmail(payboxConfigService.getPayboxConfig(company)); } } /** * Procdure permettant de vrifier que les champs de la saisie paiement necessaire Paybox sont bien remplis * @param paymentVoucher * @throws AxelorException */ public void checkPayboxPaymentVoucherFields(PaymentVoucher paymentVoucher) throws AxelorException { if (paymentVoucher.getPaidAmount() == null || paymentVoucher.getPaidAmount().compareTo(BigDecimal.ZERO) > 1) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_1), GeneralServiceImpl.EXCEPTION, paymentVoucher.getRef()), IException.CONFIGURATION_ERROR); } } /** * Procdure permettant de vrifier que le montant rgl par Paybox n'est pas suprieur au solde du payeur * @param partner * @param paidAmount * @throws AxelorException */ public void checkPaidAmount(Partner partner, Company company, BigDecimal paidAmount) throws AxelorException { AccountingSituation accountingSituation = Beans.get(AccountingSituationService.class) .getAccountingSituation(partner, company); BigDecimal partnerBalance = accountingSituation.getBalanceCustAccount(); if (paidAmount.compareTo(partnerBalance) > 0) { throw new AxelorException( String.format(I18n.get(IExceptionMessage.PAYBOX_2), GeneralServiceImpl.EXCEPTION), IException.CONFIGURATION_ERROR); } } public void checkPaidAmount(PaymentVoucher paymentVoucher) throws AxelorException { if (paymentVoucher.getRemainingAmount().compareTo(BigDecimal.ZERO) > 0) { throw new AxelorException( String.format(I18n.get(IExceptionMessage.PAYBOX_3), GeneralServiceImpl.EXCEPTION), IException.INCONSISTENCY); } } /** * Procdure permettant de vrifier que le paramtrage des champs necessaire Paybox d'un tiers est bien ralis * @param partner * @throws AxelorException */ public void checkPayboxPartnerFields(Partner partner) throws AxelorException { if (partner.getEmailAddress().getAddress() == null || partner.getEmailAddress().getAddress().isEmpty()) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_4), GeneralServiceImpl.EXCEPTION, partner.getName()), IException.CONFIGURATION_ERROR); } } /** * Fonction calculant la signature HMAC des paramtres * @param data * La chaine contenant les paramtres * @param hmacKey * La cl HMAC * @param algorithm * L'algorithme utilis (SHA512, ...) * @return * @throws AxelorException */ public String getHmacSignature(String data, String hmacKey, String algorithm) throws AxelorException { try { byte[] bytesKey = DatatypeConverter.parseHexBinary(hmacKey); SecretKeySpec secretKey = new SecretKeySpec(bytesKey, "Hmac" + algorithm); Mac mac = Mac.getInstance("Hmac" + algorithm); mac.init(secretKey); byte[] macData = mac.doFinal(data.getBytes(this.CHARSET)); // final byte[] hex = new Hex().encode( macData ); // return new String( hex, this.CHARSET ); // LOG.debug("Message HMAC 2 : {}",new String( hex, this.CHARSET )); String s = StringTool.getHexString(macData); return s.toUpperCase(); } catch (InvalidKeyException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION, e), IException.INCONSISTENCY); } catch (NoSuchAlgorithmException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION, e), IException.INCONSISTENCY); } catch (UnsupportedEncodingException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION, e), IException.INCONSISTENCY); } } /** * Mthode permettant d'ajouter une adresse email un contact * @param contact * Un contact * @param email * Une adresse email * @param toSaveOk * L'adresse email doit-elle tre enregistr pour le contact */ @Transactional(rollbackOn = { AxelorException.class, Exception.class }) public void addPayboxEmail(Partner partner, String email, boolean toSaveOk) { if (toSaveOk) { partner.getEmailAddress().setAddress(email); partnerRepository.save(partner); } } /** * * @param signature * La signture contenu dans l'url * @param varUrl * Liste des variables contenu dans l'url, priv de la dernire : la signature * @param company * La socit * @return * @throws Exception */ public boolean checkPaybox(String signature, List<String> varUrl, Company company) throws Exception { boolean result = this.checkPaybox(signature, varUrl, company.getAccountConfig().getPayboxConfig().getPayboxPublicKeyPath()); log.debug("Resultat de la verification de signature : {}", result); return result; } /** * * @param signature * La signature contenu dans l'url * @param urlParam * Liste des paramtres contenus dans l'url, priv du dernier : la signature * @param pubKeyPath * Le chemin de la cl publique Paybox * @return * @throws Exception */ public boolean checkPaybox(String signature, List<String> urlParam, String pubKeyPath) throws Exception { String payboxParams = StringUtils.join(urlParam, "&"); log.debug("Liste des variables Paybox signes : {}", payboxParams); // Dj dcode par le framework // String decoded = URLDecoder.decode(sign, this.CHARSET); byte[] sigBytes = Base64.decode(signature.getBytes(this.CHARSET)); // lecture de la cle publique PublicKey pubKey = this.getPubKey(pubKeyPath); /** * Dans le cas o le cl est au format .der * * PublicKey pubKey = this.getPubKeyDer(pubKeyPath); */ // verification signature return this.verify(payboxParams.getBytes(), sigBytes, this.HASH_ENCRYPTION_ALGORITHM, pubKey); } /** Chargement de la cle AU FORMAT pem * Alors ajouter la dpendance dans le fichier pom.xml : * <dependency> * <groupId>org.bouncycastle</groupId> * <artifactId>bcprov-jdk15on</artifactId> * <version>1.47</version> * </dependency> * * Ainsi que l'import : import org.bouncycastle.util.io.pem.PemReader; * * @param pubKeyFile * @return * @throws Exception */ private PublicKey getPubKey(String pubKeyPath) throws Exception { PemReader reader = new PemReader(new FileReader(pubKeyPath)); byte[] pubKey = reader.readPemObject().getContent(); reader.close(); KeyFactory keyFactory = KeyFactory.getInstance(this.ENCRYPTION_ALGORITHM); X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pubKey); return keyFactory.generatePublic(pubKeySpec); } /** Chargement de la cle AU FORMAT der * Utliser la commande suivante pour 'convertir' la cl 'pem' en 'der' * openssl rsa -inform PEM -in pubkey.pem -outform DER -pubin -out pubkey.der * * @param pubKeyFile * @return * @throws Exception */ @Deprecated private PublicKey getPubKeyDer(String pubKeyPath) throws Exception { FileInputStream fis = new FileInputStream(pubKeyPath); DataInputStream dis = new DataInputStream(fis); byte[] pubKeyBytes = new byte[fis.available()]; dis.readFully(pubKeyBytes); fis.close(); dis.close(); KeyFactory keyFactory = KeyFactory.getInstance(this.ENCRYPTION_ALGORITHM); // extraction cle X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pubKeyBytes); return keyFactory.generatePublic(pubSpec); } /** * verification signature RSA des donnees avec cle publique * @param dataBytes * @param sigBytes * @param pubKey * @return * @throws Exception */ private boolean verify(byte[] dataBytes, byte[] sigBytes, String sigAlg, PublicKey pubKey) throws Exception { Signature signature = Signature.getInstance(sigAlg); signature.initVerify(pubKey); signature.update(dataBytes); return signature.verify(sigBytes); } }