| /******************************************************************************* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| *******************************************************************************/ |
| package org.apache.ofbiz.accounting.thirdparty.valuelink; |
| |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyFactory; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.PublicKey; |
| import java.security.SecureRandom; |
| import java.security.spec.InvalidKeySpecException; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Random; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.KeyAgreement; |
| import javax.crypto.KeyGenerator; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.SecretKey; |
| import javax.crypto.SecretKeyFactory; |
| import javax.crypto.interfaces.DHPrivateKey; |
| import javax.crypto.interfaces.DHPublicKey; |
| import javax.crypto.spec.DESKeySpec; |
| import javax.crypto.spec.DESedeKeySpec; |
| import javax.crypto.spec.DHParameterSpec; |
| import javax.crypto.spec.DHPrivateKeySpec; |
| import javax.crypto.spec.DHPublicKeySpec; |
| import javax.crypto.spec.IvParameterSpec; |
| |
| import org.apache.ofbiz.base.util.Debug; |
| import org.apache.ofbiz.base.util.HttpClient; |
| import org.apache.ofbiz.base.util.HttpClientException; |
| import org.apache.ofbiz.base.util.StringUtil; |
| import org.apache.ofbiz.base.util.UtilValidate; |
| import org.apache.ofbiz.entity.Delegator; |
| import org.apache.ofbiz.entity.GenericEntityException; |
| import org.apache.ofbiz.entity.GenericValue; |
| import org.apache.ofbiz.entity.util.EntityQuery; |
| |
| /** |
| * ValueLinkApi - Implementation of ValueLink Encryption and Transport |
| */ |
| public class ValueLinkApi { |
| |
| public static final String module = ValueLinkApi.class.getName(); |
| |
| // static object cache |
| private static Map<String, Object> objectCache = new HashMap<String, Object>(); |
| |
| // instance variables |
| protected Delegator delegator = null; |
| protected Properties props = null; |
| protected SecretKey kek = null; |
| protected SecretKey mwk = null; |
| protected String merchantId = null; |
| protected String terminalId = null; |
| protected Long mwkIndex = null; |
| protected boolean debug = false; |
| |
| protected ValueLinkApi() {} |
| protected ValueLinkApi(Delegator delegator, Properties props) { |
| String mId = (String) props.get("payment.valuelink.merchantId"); |
| String tId = (String) props.get("payment.valuelink.terminalId"); |
| this.delegator = delegator; |
| this.merchantId = mId; |
| this.terminalId = tId; |
| this.props = props; |
| if ("Y".equalsIgnoreCase((String) props.get("payment.valuelink.debug"))) { |
| this.debug = true; |
| } |
| |
| if (debug) { |
| Debug.logInfo("New ValueLinkApi instance created", module); |
| Debug.logInfo("Merchant ID : " + merchantId, module); |
| Debug.logInfo("Terminal ID : " + terminalId, module); |
| } |
| } |
| |
| /** |
| * Obtain an instance of the ValueLinkApi |
| * @param delegator Delegator used to query the encryption keys |
| * @param props Properties to use for the Api (usually payment.properties) |
| * @param reload When true, will replace an existing instance in the cache and reload all properties |
| * @return ValueLinkApi reference |
| */ |
| public static ValueLinkApi getInstance(Delegator delegator, Properties props, boolean reload) { |
| if (props == null) { |
| throw new IllegalArgumentException("Properties cannot be null"); |
| } |
| String merchantId = (String) props.get("payment.valuelink.merchantId"); |
| |
| ValueLinkApi api = (ValueLinkApi) objectCache.get(merchantId); |
| if (api == null) { |
| throw new RuntimeException("Runtime problems with ValueLinkApi; unable to create instance"); |
| } |
| if (reload) { |
| synchronized(ValueLinkApi.class) { |
| api = (ValueLinkApi) objectCache.get(merchantId); |
| if (api == null) { |
| api = new ValueLinkApi(delegator, props); |
| objectCache.put(merchantId, api); |
| } |
| } |
| } |
| |
| return api; |
| } |
| |
| /** |
| * Obtain an instance of the ValueLinkApi; this method will always return an existing reference if one is available |
| * @param delegator Delegator used to query the encryption keys |
| * @param props Properties to use for the Api (usually payment.properties) |
| * @return Obtain an instance of the ValueLinkApi |
| */ |
| public static ValueLinkApi getInstance(Delegator delegator, Properties props) { |
| return getInstance(delegator, props, false); |
| } |
| |
| /** |
| * Encrypt the defined pin using the configured keys |
| * @param pin Plain text String of the pin |
| * @return Hex String of the encrypted pin (EAN) for transmission to ValueLink |
| */ |
| public String encryptPin(String pin) { |
| // get the Cipher |
| Cipher mwkCipher = this.getCipher(this.getMwkKey(), Cipher.ENCRYPT_MODE); |
| |
| // pin to bytes |
| byte[] pinBytes = pin.getBytes(); |
| |
| // 7 bytes of random data |
| byte[] random = this.getRandomBytes(7); |
| |
| // pin checksum |
| byte[] checkSum = this.getPinCheckSum(pinBytes); |
| |
| // put all together |
| byte[] eanBlock = new byte[16]; |
| int i; |
| for (i = 0; i < random.length; i++) { |
| eanBlock[i] = random[i]; |
| } |
| eanBlock[7] = checkSum[0]; |
| for (i = 0; i < pinBytes.length; i++) { |
| eanBlock[i + 8] = pinBytes[i]; |
| } |
| |
| // encrypy the ean |
| String encryptedEanHex = null; |
| try { |
| byte[] encryptedEan = mwkCipher.doFinal(eanBlock); |
| encryptedEanHex = StringUtil.toHexString(encryptedEan); |
| } catch (IllegalStateException e) { |
| Debug.logError(e, module); |
| } catch (IllegalBlockSizeException e) { |
| Debug.logError(e, module); |
| } catch (BadPaddingException e) { |
| Debug.logError(e, module); |
| } |
| |
| if (debug) { |
| Debug.logInfo("encryptPin : " + pin + " / " + encryptedEanHex, module); |
| } |
| |
| return encryptedEanHex; |
| } |
| |
| /** |
| * Decrypt an encrypted pin using the configured keys |
| * @param pin Hex String of the encrypted pin (EAN) |
| * @return Plain text String of the pin |
| */ |
| public String decryptPin(String pin) { |
| // get the Cipher |
| Cipher mwkCipher = this.getCipher(this.getMwkKey(), Cipher.DECRYPT_MODE); |
| |
| // decrypt pin |
| String decryptedPinString = null; |
| try { |
| byte[] decryptedEan = mwkCipher.doFinal(StringUtil.fromHexString(pin)); |
| byte[] decryptedPin = getByteRange(decryptedEan, 8, 8); |
| decryptedPinString = new String(decryptedPin); |
| } catch (IllegalStateException e) { |
| Debug.logError(e, module); |
| } catch (IllegalBlockSizeException e) { |
| Debug.logError(e, module); |
| } catch (BadPaddingException e) { |
| Debug.logError(e, module); |
| } |
| |
| if (debug) { |
| Debug.logInfo("decryptPin : " + pin + " / " + decryptedPinString, module); |
| } |
| |
| return decryptedPinString; |
| } |
| |
| /** |
| * Transmit a request to ValueLink |
| * @param request Map of request parameters |
| * @return Map of response parameters |
| * @throws HttpClientException |
| */ |
| public Map<String, Object> send(Map<String, Object> request) throws HttpClientException { |
| return send((String) props.get("payment.valuelink.url"), request); |
| } |
| |
| /** |
| * Transmit a request to ValueLink |
| * @param url override URL from what is defined in the properties |
| * @param request request Map of request parameters |
| * @return Map of response parameters |
| * @throws HttpClientException |
| */ |
| public Map<String, Object> send(String url, Map<String, Object> request) throws HttpClientException { |
| if (debug) { |
| Debug.logInfo("Request : " + url + " / " + request, module); |
| } |
| |
| // read the timeout value |
| String timeoutString = (String) props.get("payment.valuelink.timeout"); |
| int timeout = 34; |
| try { |
| timeout = Integer.parseInt(timeoutString); |
| } catch (NumberFormatException e) { |
| Debug.logError(e, "Unable to set timeout to " + timeoutString + " using default " + timeout); |
| } |
| |
| // create the HTTP client |
| HttpClient client = new HttpClient(url, request); |
| client.setTimeout(timeout * 1000); |
| client.setDebug(debug); |
| |
| client.setClientCertificateAlias((String) props.get("payment.valuelink.certificateAlias")); |
| String response = client.post(); |
| |
| // parse the response and return a map |
| return this.parseResponse(response); |
| } |
| |
| /** |
| * Output the creation of public/private keys + KEK to the console for manual database update |
| */ |
| public StringBuffer outputKeyCreation(boolean kekOnly, String kekTest) { |
| return this.outputKeyCreation(0, kekOnly, kekTest); |
| } |
| |
| private StringBuffer outputKeyCreation(int loop, boolean kekOnly, String kekTest) { |
| StringBuffer buf = new StringBuffer(); |
| loop++; |
| |
| if (loop > 100) { |
| // only loop 100 times; then throw an exception |
| throw new IllegalStateException("Unable to create 128 byte keys in 100 tries"); |
| } |
| |
| // place holder for the keys |
| DHPrivateKey privateKey = null; |
| DHPublicKey publicKey = null; |
| |
| if (!kekOnly) { |
| KeyPair keyPair = null; |
| try { |
| keyPair = this.createKeys(); |
| } catch (NoSuchAlgorithmException e) { |
| Debug.logError(e, module); |
| } catch (InvalidAlgorithmParameterException e) { |
| Debug.logError(e, module); |
| } catch (InvalidKeySpecException e) { |
| Debug.logError(e, module); |
| } |
| |
| if (keyPair != null) { |
| publicKey = (DHPublicKey) keyPair.getPublic(); |
| privateKey = (DHPrivateKey) keyPair.getPrivate(); |
| |
| if (publicKey == null || publicKey.getY().toByteArray().length != 128) { |
| // run again until we get a 128 byte public key for VL |
| return this.outputKeyCreation(loop, kekOnly, kekTest); |
| } |
| } else { |
| Debug.logInfo("Returned a null KeyPair", module); |
| return this.outputKeyCreation(loop, kekOnly, kekTest); |
| } |
| } else { |
| // use our existing private key to generate a KEK |
| try { |
| privateKey = (DHPrivateKey) this.getPrivateKey(); |
| } catch (Exception e) { |
| Debug.logError(e, module); |
| } |
| } |
| |
| // the KEK |
| byte[] kekBytes = null; |
| try { |
| kekBytes = this.generateKek(privateKey); |
| } catch (NoSuchAlgorithmException e) { |
| Debug.logError(e, module); |
| } catch (InvalidKeySpecException e) { |
| Debug.logError(e, module); |
| } catch (InvalidKeyException e) { |
| Debug.logError(e, module); |
| } |
| |
| // the 3DES KEK value |
| SecretKey loadedKek = this.getDesEdeKey(kekBytes); |
| byte[] loadKekBytes = loadedKek.getEncoded(); |
| |
| // test the KEK |
| Cipher cipher = this.getCipher(this.getKekKey(), Cipher.ENCRYPT_MODE); |
| byte[] kekTestB = { 0, 0, 0, 0, 0, 0, 0, 0 }; |
| byte[] kekTestC = new byte[0]; |
| if (kekTest != null) { |
| kekTestB = StringUtil.fromHexString(kekTest); |
| } |
| |
| // encrypt the test bytes |
| try { |
| kekTestC = cipher.doFinal(kekTestB); |
| } catch (Exception e) { |
| Debug.logError(e, module); |
| } |
| |
| if (!kekOnly) { |
| // public key (just Y) |
| BigInteger y = publicKey.getY(); |
| byte[] yBytes = y.toByteArray(); |
| String yHex = StringUtil.toHexString(yBytes); |
| buf.append("======== Begin Public Key (Y @ ").append(yBytes.length).append(" / ").append(yHex.length()).append(") ========\n"); |
| buf.append(yHex).append("\n"); |
| buf.append("======== End Public Key ========\n\n"); |
| |
| // private key (just X) |
| BigInteger x = privateKey.getX(); |
| byte[] xBytes = x.toByteArray(); |
| String xHex = StringUtil.toHexString(xBytes); |
| buf.append("======== Begin Private Key (X @ ").append(xBytes.length).append(" / ").append(xHex.length()).append(") ========\n"); |
| buf.append(xHex).append("\n"); |
| buf.append("======== End Private Key ========\n\n"); |
| |
| // private key (full) |
| byte[] privateBytes = privateKey.getEncoded(); |
| String privateHex = StringUtil.toHexString(privateBytes); |
| buf.append("======== Begin Private Key (Full @ ").append(privateBytes.length).append(" / ").append(privateHex.length()).append(") ========\n"); |
| buf.append(privateHex).append("\n"); |
| buf.append("======== End Private Key ========\n\n"); |
| } |
| |
| if (kekBytes != null) { |
| buf.append("======== Begin KEK (").append(kekBytes.length).append(") ========\n"); |
| buf.append(StringUtil.toHexString(kekBytes)).append("\n"); |
| buf.append("======== End KEK ========\n\n"); |
| |
| buf.append("======== Begin KEK (DES) (").append(loadKekBytes.length).append(") ========\n"); |
| buf.append(StringUtil.toHexString(loadKekBytes)).append("\n"); |
| buf.append("======== End KEK (DES) ========\n\n"); |
| |
| buf.append("======== Begin KEK Test (").append(kekTestC.length).append(") ========\n"); |
| buf.append(StringUtil.toHexString(kekTestC)).append("\n"); |
| buf.append("======== End KEK Test ========\n\n"); |
| } else { |
| Debug.logError("KEK came back empty", module); |
| } |
| |
| return buf; |
| } |
| |
| /** |
| * Create a set of public/private keys using ValueLinks defined parameters |
| * @return KeyPair object containing both public and private keys |
| * @throws NoSuchAlgorithmException |
| * @throws InvalidAlgorithmParameterException |
| */ |
| public KeyPair createKeys() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeySpecException { |
| // initialize the parameter spec |
| DHPublicKey publicKey = (DHPublicKey) this.getValueLinkPublicKey(); |
| DHParameterSpec dhParamSpec = publicKey.getParams(); |
| // create the public/private key pair using parameters defined by valuelink |
| KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH"); |
| keyGen.initialize(dhParamSpec); |
| KeyPair keyPair = keyGen.generateKeyPair(); |
| |
| return keyPair; |
| } |
| |
| /** |
| * Generate a key exchange key for use in encrypting the mwk |
| * @param privateKey The private key for the merchant |
| * @return byte array containing the kek |
| * @throws NoSuchAlgorithmException |
| * @throws InvalidKeySpecException |
| * @throws InvalidKeyException |
| */ |
| public byte[] generateKek(PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { |
| // get the ValueLink public key |
| PublicKey vlPublic = this.getValueLinkPublicKey(); |
| |
| // generate shared secret key |
| KeyAgreement ka = KeyAgreement.getInstance("DH"); |
| ka.init(privateKey); |
| ka.doPhase(vlPublic, true); |
| byte[] secretKey = ka.generateSecret(); |
| |
| if (debug) { |
| Debug.logInfo("Secret Key : " + StringUtil.toHexString(secretKey) + " / " + secretKey.length, module); |
| } |
| |
| // generate 3DES from secret key using VL algorithm (KEK) |
| MessageDigest md = MessageDigest.getInstance("SHA1"); |
| byte[] digest = md.digest(secretKey); |
| byte[] des2 = getByteRange(digest, 0, 16); |
| byte[] first8 = getByteRange(des2, 0, 8); |
| byte[] kek = copyBytes(des2, first8, 0); |
| |
| if (debug) { |
| Debug.logInfo("Generated KEK : " + StringUtil.toHexString(kek) + " / " + kek.length, module); |
| } |
| |
| return kek; |
| } |
| |
| /** |
| * Get a public key object for the ValueLink supplied public key |
| * @return PublicKey object of ValueLinks's public key |
| * @throws NoSuchAlgorithmException |
| * @throws InvalidKeySpecException |
| */ |
| public PublicKey getValueLinkPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { |
| // read the valuelink public key |
| String publicValue = (String) props.get("payment.valuelink.publicValue"); |
| byte[] publicKeyBytes = StringUtil.fromHexString(publicValue); |
| |
| // initialize the parameter spec |
| DHParameterSpec dhParamSpec = this.getDHParameterSpec(); |
| |
| // load the valuelink public key |
| KeyFactory keyFactory = KeyFactory.getInstance("DH"); |
| BigInteger publicKeyInt = new BigInteger(publicKeyBytes); |
| DHPublicKeySpec dhPublicSpec = new DHPublicKeySpec(publicKeyInt, dhParamSpec.getP(), dhParamSpec.getG()); |
| PublicKey vlPublic = keyFactory.generatePublic(dhPublicSpec); |
| |
| return vlPublic; |
| } |
| |
| /** |
| * Get merchant Private Key |
| * @return PrivateKey object for the merchant |
| */ |
| public PrivateKey getPrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException { |
| byte[] privateKeyBytes = this.getPrivateKeyBytes(); |
| |
| // initialize the parameter spec |
| DHParameterSpec dhParamSpec = this.getDHParameterSpec(); |
| |
| // load the private key |
| KeyFactory keyFactory = KeyFactory.getInstance("DH"); |
| BigInteger privateKeyInt = new BigInteger(privateKeyBytes); |
| DHPrivateKeySpec dhPrivateSpec = new DHPrivateKeySpec(privateKeyInt, dhParamSpec.getP(), dhParamSpec.getG()); |
| PrivateKey privateKey = keyFactory.generatePrivate(dhPrivateSpec); |
| |
| return privateKey; |
| } |
| |
| /** |
| * Generate a new MWK |
| * @return Hex String of the new encrypted MWK ready for transmission to ValueLink |
| */ |
| public byte[] generateMwk() { |
| KeyGenerator keyGen = null; |
| try { |
| keyGen = KeyGenerator.getInstance("DES"); |
| } catch (NoSuchAlgorithmException e) { |
| Debug.logError(e, module); |
| } |
| |
| // generate the DES key 1 |
| SecretKey des1 = keyGen.generateKey(); |
| SecretKey des2 = keyGen.generateKey(); |
| |
| if (des1 != null && des2 != null) { |
| byte[] desByte1 = des1.getEncoded(); |
| byte[] desByte2 = des2.getEncoded(); |
| byte[] desByte3 = des1.getEncoded(); |
| |
| // check for weak keys |
| try { |
| if (DESKeySpec.isWeak(des1.getEncoded(), 0) || DESKeySpec.isWeak(des2.getEncoded(), 0)) { |
| return generateMwk(); |
| } |
| } catch (Exception e) { |
| Debug.logError(e, module); |
| } |
| |
| byte[] des3 = copyBytes(desByte1, copyBytes(desByte2, desByte3, 0), 0); |
| return generateMwk(des3); |
| } else { |
| Debug.logInfo("Null DES keys returned", module); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Generate a new MWK |
| * @param desBytes byte array of the DES key (24 bytes) |
| * @return Hex String of the new encrypted MWK ready for transmission to ValueLink |
| */ |
| public byte[] generateMwk(byte[] desBytes) { |
| if (debug) { |
| Debug.logInfo("DES Key : " + StringUtil.toHexString(desBytes) + " / " + desBytes.length, module); |
| } |
| SecretKeyFactory skf1 = null; |
| SecretKey mwk = null; |
| try { |
| skf1 = SecretKeyFactory.getInstance("DESede"); |
| } catch (NoSuchAlgorithmException e) { |
| Debug.logError(e, module); |
| } |
| DESedeKeySpec desedeSpec2 = null; |
| try { |
| desedeSpec2 = new DESedeKeySpec(desBytes); |
| } catch (InvalidKeyException e) { |
| Debug.logError(e, module); |
| } |
| if (skf1 != null && desedeSpec2 != null) { |
| try { |
| mwk = skf1.generateSecret(desedeSpec2); |
| } catch (InvalidKeySpecException e) { |
| Debug.logError(e, module); |
| } |
| } |
| if (mwk != null) { |
| return generateMwk(mwk); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Generate a new MWK |
| * @param mwkdes3 pre-generated DES3 SecretKey |
| * @return Hex String of the new encrypted MWK ready for transmission to ValueLink |
| */ |
| public byte[] generateMwk(SecretKey mwkdes3) { |
| // zeros for checksum |
| byte[] zeros = { 0, 0, 0, 0, 0, 0, 0, 0 }; |
| |
| // 8 bytes random data |
| byte[] random = new byte[8]; |
| Random ran = new SecureRandom(); |
| ran.nextBytes(random); |
| |
| |
| // open a cipher using the new mwk |
| Cipher cipher = this.getCipher(mwkdes3, Cipher.ENCRYPT_MODE); |
| |
| // make the checksum - encrypted 8 bytes of 0's |
| byte[] encryptedZeros = new byte[0]; |
| try { |
| encryptedZeros = cipher.doFinal(zeros); |
| } catch (IllegalStateException e) { |
| Debug.logError(e, module); |
| } catch (IllegalBlockSizeException e) { |
| Debug.logError(e, module); |
| } catch (BadPaddingException e) { |
| Debug.logError(e, module); |
| } |
| |
| // make the 40 byte MWK - random 8 bytes + key + checksum |
| byte[] newMwk = copyBytes(mwkdes3.getEncoded(), encryptedZeros, 0); |
| newMwk = copyBytes(random, newMwk, 0); |
| |
| if (debug) { |
| Debug.logInfo("Random 8 byte : " + StringUtil.toHexString(random), module); |
| Debug.logInfo("Encrypted 0's : " + StringUtil.toHexString(encryptedZeros), module); |
| Debug.logInfo("Decrypted MWK : " + StringUtil.toHexString(mwkdes3.getEncoded()) + " / " + mwkdes3.getEncoded().length, module); |
| Debug.logInfo("Encrypted MWK : " + StringUtil.toHexString(newMwk) + " / " + newMwk.length, module); |
| } |
| |
| return newMwk; |
| } |
| |
| /** |
| * Use the KEK to encrypt a value usually the MWK |
| * @param content byte array to encrypt |
| * @return encrypted byte array |
| */ |
| public byte[] encryptViaKek(byte[] content) { |
| return cryptoViaKek(content, Cipher.ENCRYPT_MODE); |
| } |
| |
| /** |
| * Ue the KEK to decrypt a value |
| * @param content byte array to decrypt |
| * @return decrypted byte array |
| */ |
| public byte[] decryptViaKek(byte[] content) { |
| return cryptoViaKek(content, Cipher.DECRYPT_MODE); |
| } |
| |
| /** |
| * Returns a date string formatted as directed by ValueLink |
| * @return ValueLink formatted date String |
| */ |
| public String getDateString() { |
| String format = (String) props.get("payment.valuelink.timestamp"); |
| SimpleDateFormat sdf = new SimpleDateFormat(format); |
| return sdf.format(new Date()); |
| } |
| |
| /** |
| * Returns the current working key index |
| * @return Long number of the current working key index |
| */ |
| public Long getWorkingKeyIndex() { |
| if (this.mwkIndex == null) { |
| synchronized(this) { |
| if (this.mwkIndex == null) { |
| this.mwkIndex = this.getGenericValue().getLong("workingKeyIndex"); |
| } |
| } |
| } |
| |
| if (debug) { |
| Debug.logInfo("Current Working Key Index : " + this.mwkIndex, module); |
| } |
| |
| return this.mwkIndex; |
| } |
| |
| /** |
| * Returns a ValueLink formatted amount String |
| * @param amount BigDecimal value to format |
| * @return Formatted String |
| */ |
| public String getAmount(BigDecimal amount) { |
| if (amount == null) { |
| return "0.00"; |
| } |
| return Integer.toString(amount.movePointRight(2).intValue()); |
| } |
| |
| /** |
| * Returns a BigDecimal from a ValueLink formatted amount String |
| * @param amount The ValueLink formatted amount String |
| * @return BigDecimal object |
| */ |
| public BigDecimal getAmount(String amount) { |
| if (amount == null) { |
| return BigDecimal.ZERO; |
| } |
| BigDecimal amountBd = new BigDecimal(amount); |
| return amountBd.movePointLeft(2); |
| } |
| |
| public String getCurrency(String currency) { |
| return "840"; // todo make this multi-currency |
| } |
| |
| /** |
| * Creates a Map of initial request values (MerchID, AltMerchNo, Modes, MerchTime, TermTxnNo, EncryptID) |
| * Note: For 2010 (assign working key) transaction, the EncryptID will need to be adjusted |
| * @return Map containing the inital request values |
| */ |
| public Map<String, Object> getInitialRequestMap(Map<String, Object> context) { |
| Map<String, Object> request = new HashMap<String, Object>(); |
| |
| // merchant information |
| request.put("MerchID", merchantId + terminalId); |
| request.put("AltMerchNo", props.get("payment.valuelink.altMerchantId")); |
| |
| // mode settings |
| String modes = (String) props.get("payment.valuelink.modes"); |
| if (UtilValidate.isNotEmpty(modes)) { |
| request.put("Modes", modes); |
| } |
| |
| // merchant timestamp |
| String merchTime = (String) context.get("MerchTime"); |
| if (merchTime == null) { |
| merchTime = this.getDateString(); |
| } |
| request.put("MerchTime", merchTime); |
| |
| // transaction number |
| String termTxNo = (String) context.get("TermTxnNo"); |
| if (termTxNo == null) { |
| termTxNo = delegator.getNextSeqId("ValueLinkKey"); |
| } |
| request.put("TermTxnNo", termTxNo); |
| |
| // current working key index |
| request.put("EncryptID", this.getWorkingKeyIndex()); |
| |
| if (debug) { |
| Debug.logInfo("Created Initial Request Map : " + request, module); |
| } |
| |
| return request; |
| } |
| |
| /** |
| * Gets the cached value object for this merchant's keys |
| * @return Cached GenericValue object |
| */ |
| public GenericValue getGenericValue() { |
| GenericValue value = null; |
| try { |
| value = EntityQuery.use(delegator).from("ValueLinkKey").where("merchantId", merchantId).cache().queryOne(); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, module); |
| } |
| if (value == null) { |
| throw new RuntimeException("No ValueLinkKey record found for Merchant ID : " + merchantId); |
| } |
| return value; |
| } |
| |
| /** |
| * Reloads the keys in the object cache; use this when re-creating keys |
| */ |
| public void reload() { |
| this.kek = null; |
| this.mwk = null; |
| this.mwkIndex = null; |
| } |
| |
| // using the prime and generator provided by valuelink; create a parameter object |
| protected DHParameterSpec getDHParameterSpec() { |
| String primeHex = (String) props.get("payment.valuelink.prime"); |
| String genString = (String) props.get("payment.valuelink.generator"); |
| |
| // convert the p/g hex values |
| byte[] primeByte = StringUtil.fromHexString(primeHex); |
| BigInteger prime = new BigInteger(1, primeByte); // force positive (unsigned) |
| BigInteger generator = new BigInteger(genString); |
| |
| // initialize the parameter spec |
| DHParameterSpec dhParamSpec = new DHParameterSpec(prime, generator, 1024); |
| |
| return dhParamSpec; |
| } |
| |
| // actual kek encryption/decryption code |
| protected byte[] cryptoViaKek(byte[] content, int mode) { |
| // open a cipher using the kek for transport |
| Cipher cipher = this.getCipher(this.getKekKey(), mode); |
| byte[] dec = new byte[0]; |
| try { |
| dec = cipher.doFinal(content); |
| } catch (IllegalStateException e) { |
| Debug.logError(e, module); |
| } catch (IllegalBlockSizeException e) { |
| Debug.logError(e, module); |
| } catch (BadPaddingException e) { |
| Debug.logError(e, module); |
| } |
| return dec; |
| } |
| |
| // return a cipher for a key - DESede/CBC/NoPadding IV = 0 |
| protected Cipher getCipher(SecretKey key, int mode) { |
| byte[] zeros = { 0, 0, 0, 0, 0, 0, 0, 0 }; |
| IvParameterSpec iv = new IvParameterSpec(zeros); |
| |
| // create the Cipher - DESede/CBC/NoPadding |
| Cipher mwkCipher = null; |
| try { |
| mwkCipher = Cipher.getInstance("DESede/CBC/NoPadding"); |
| } catch (NoSuchAlgorithmException e) { |
| Debug.logError(e, module); |
| return null; |
| } catch (NoSuchPaddingException e) { |
| Debug.logError(e, module); |
| } |
| try { |
| mwkCipher.init(mode, key, iv); |
| } catch (InvalidKeyException e) { |
| Debug.logError(e, "Invalid key", module); |
| } catch (InvalidAlgorithmParameterException e) { |
| Debug.logError(e, module); |
| } |
| return mwkCipher; |
| } |
| |
| protected byte[] getPinCheckSum(byte[] pinBytes) { |
| byte[] checkSum = new byte[1]; |
| checkSum[0] = 0; |
| for (int i = 0; i < pinBytes.length; i++) { |
| checkSum[0] += pinBytes[i]; |
| } |
| return checkSum; |
| } |
| |
| protected byte[] getRandomBytes(int length) { |
| Random rand = new SecureRandom(); |
| byte[] randomBytes = new byte[length]; |
| rand.nextBytes(randomBytes); |
| return randomBytes; |
| } |
| |
| protected SecretKey getMwkKey() { |
| if (mwk == null) { |
| mwk = this.getDesEdeKey(getByteRange(getMwk(), 8, 24)); |
| } |
| |
| if (debug) { |
| Debug.logInfo("Raw MWK : " + StringUtil.toHexString(getMwk()), module); |
| Debug.logInfo("MWK : " + StringUtil.toHexString(mwk.getEncoded()), module); |
| } |
| |
| return mwk; |
| } |
| |
| protected SecretKey getKekKey() { |
| if (kek == null) { |
| kek = this.getDesEdeKey(getKek()); |
| } |
| |
| if (debug) { |
| Debug.logInfo("Raw KEK : " + StringUtil.toHexString(getKek()), module); |
| Debug.logInfo("KEK : " + StringUtil.toHexString(kek.getEncoded()), module); |
| } |
| |
| return kek; |
| } |
| |
| protected SecretKey getDesEdeKey(byte[] rawKey) { |
| SecretKeyFactory skf = null; |
| try { |
| skf = SecretKeyFactory.getInstance("DESede"); |
| } catch (NoSuchAlgorithmException e) { |
| // should never happen since DESede is a standard algorithm |
| Debug.logError(e, module); |
| return null; |
| } |
| |
| // load the raw key |
| if (rawKey.length > 0) { |
| DESedeKeySpec desedeSpec1 = null; |
| try { |
| desedeSpec1 = new DESedeKeySpec(rawKey); |
| } catch (InvalidKeyException e) { |
| Debug.logError(e, "Not a valid DESede key", module); |
| return null; |
| } |
| |
| // create the SecretKey Object |
| SecretKey key = null; |
| try { |
| key = skf.generateSecret(desedeSpec1); |
| } catch (InvalidKeySpecException e) { |
| Debug.logError(e, module); |
| } |
| return key; |
| } else { |
| throw new RuntimeException("No valid DESede key available"); |
| } |
| } |
| |
| protected byte[] getMwk() { |
| return StringUtil.fromHexString(this.getGenericValue().getString("workingKey")); |
| } |
| |
| protected byte[] getKek() { |
| return StringUtil.fromHexString(this.getGenericValue().getString("exchangeKey")); |
| } |
| |
| protected byte[] getPrivateKeyBytes() { |
| return StringUtil.fromHexString(this.getGenericValue().getString("privateKey")); |
| } |
| |
| protected Map<String, Object> parseResponse(String response) { |
| if (debug) { |
| Debug.logInfo("Raw Response : " + response, module); |
| } |
| |
| // covert to all lowercase and trim off the html header |
| String subResponse = response.toLowerCase(); |
| int firstIndex = subResponse.indexOf("<tr>"); |
| int lastIndex = subResponse.lastIndexOf("</tr>"); |
| subResponse = subResponse.substring(firstIndex, lastIndex); |
| |
| // check for a history table |
| String history = null; |
| List<Map<String, String>> historyMapList = null; |
| if (subResponse.indexOf("<table") > -1) { |
| int startHistory = subResponse.indexOf("<table"); |
| int endHistory = subResponse.indexOf("</table>") + 8; |
| history = subResponse.substring(startHistory, endHistory); |
| |
| // replace the subResponse string so it doesn't conflict |
| subResponse = StringUtil.replaceString(subResponse, history, "[_HISTORY_]"); |
| |
| // parse the history into a list of maps |
| historyMapList = this.parseHistoryResponse(history); |
| } |
| |
| // replace all end rows with | this is the name delimiter |
| subResponse = StringUtil.replaceString(subResponse, "</tr>", "|"); |
| |
| // replace all </TD><TD> with = this is the value delimiter |
| subResponse = StringUtil.replaceString(subResponse, "</td><td>", "="); |
| |
| // clean off a bunch of other useless stuff |
| subResponse = StringUtil.replaceString(subResponse, "<tr>", ""); |
| subResponse = StringUtil.replaceString(subResponse, "<td>", ""); |
| subResponse = StringUtil.replaceString(subResponse, "</td>", ""); |
| |
| // make the map |
| Map<String, Object> responseMap = new HashMap<String, Object>(); |
| responseMap.putAll(StringUtil.strToMap(subResponse, true)); |
| |
| // add the raw html back in just in case we need it later |
| responseMap.put("_rawHtmlResponse", response); |
| |
| // if we have a history add it back in |
| if (history != null) { |
| responseMap.put("_rawHistoryHtml", history); |
| responseMap.put("history", historyMapList); |
| } |
| |
| if (debug) { |
| Debug.logInfo("Response Map : " + responseMap, module); |
| } |
| |
| return responseMap; |
| } |
| |
| private List<Map<String, String>> parseHistoryResponse(String response) { |
| if (debug) { |
| Debug.logInfo("Raw History : " + response, module); |
| } |
| |
| // covert to all lowercase and trim off the html header |
| String subResponse = response.toLowerCase(); |
| int firstIndex = subResponse.indexOf("<tr>"); |
| int lastIndex = subResponse.lastIndexOf("</tr>"); |
| subResponse = subResponse.substring(firstIndex, lastIndex); |
| |
| // clean up the html and replace the delimiters with '|' |
| subResponse = StringUtil.replaceString(subResponse, "<td>", ""); |
| subResponse = StringUtil.replaceString(subResponse, "</td>", "|"); |
| |
| // test the string to make sure we have fields to parse |
| String testResponse = StringUtil.replaceString(subResponse, "<tr>", ""); |
| testResponse = StringUtil.replaceString(testResponse, "</tr>", ""); |
| testResponse = StringUtil.replaceString(testResponse, "|", ""); |
| testResponse = testResponse.trim(); |
| if (testResponse.length() == 0) { |
| if (debug) { |
| Debug.logInfo("History did not contain any fields, returning null", module); |
| } |
| return null; |
| } |
| |
| // break up the keys from the values |
| int valueStart = subResponse.indexOf("</tr>"); |
| String keys = subResponse.substring(4, valueStart - 1); |
| String values = subResponse.substring(valueStart + 9, subResponse.length() - 6); |
| |
| // split sets of values up |
| values = StringUtil.replaceString(values, "|</tr><tr>", "&"); |
| List<String> valueList = StringUtil.split(values, "&"); |
| |
| // create a List of Maps for each set of values |
| List<Map<String, String>> valueMap = new LinkedList<Map<String,String>>(); |
| for (int i = 0; i < valueList.size(); i++) { |
| valueMap.add(StringUtil.createMap(StringUtil.split(keys, "|"), StringUtil.split(valueList.get(i), "|"))); |
| } |
| |
| if (debug) { |
| Debug.logInfo("History Map : " + valueMap, module); |
| } |
| |
| return valueMap; |
| } |
| |
| /** |
| * Returns a new byte[] from the offset of the defined byte[] with a specific number of bytes |
| * @param bytes The byte[] to extract from |
| * @param offset The starting postition |
| * @param length The number of bytes to copy |
| * @return a new byte[] |
| */ |
| public static byte[] getByteRange(byte[] bytes, int offset, int length) { |
| byte[] newBytes = new byte[length]; |
| for (int i = 0; i < length; i++) { |
| newBytes[i] = bytes[offset + i]; |
| } |
| return newBytes; |
| } |
| |
| /** |
| * Copies a byte[] into another byte[] starting at a specific position |
| * @param source byte[] to copy from |
| * @param target byte[] coping into |
| * @param position the position on target where source will be copied to |
| * @return a new byte[] |
| */ |
| public static byte[] copyBytes(byte[] source, byte[] target, int position) { |
| byte[] newBytes = new byte[target.length + source.length]; |
| for (int i = 0, n = 0, x = 0; i < newBytes.length; i++) { |
| if (i < position || i > (position + source.length - 2)) { |
| newBytes[i] = target[n]; |
| n++; |
| } else { |
| for (; x < source.length; x++) { |
| newBytes[i] = source[x]; |
| if (source.length - 1 > x) { |
| i++; |
| } |
| } |
| } |
| } |
| return newBytes; |
| } |
| } |