All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jpos.security.jceadapter.JCESecurityModule Maven / Gradle / Ivy

Go to download

jPOS is an ISO-8583 based financial transaction library/framework that can be customized and extended in order to implement financial interchanges.

There is a newer version: 3.0.0
Show newest version
/*
 * jPOS Project [http://jpos.org]
 * Copyright (C) 2000-2020 jPOS Software SRL
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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 .
 */

package  org.jpos.security.jceadapter;

import org.javatuples.Pair;
import org.jpos.core.Configuration;
import org.jpos.core.ConfigurationException;
import org.jpos.core.Environment;
import org.jpos.iso.ISODate;
import org.jpos.iso.ISOException;
import org.jpos.iso.ISOUtil;
import org.jpos.security.*;
import org.jpos.util.LogEvent;
import org.jpos.util.Logger;
import org.jpos.util.SimpleMsg;

import javax.crypto.SecretKey;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Cipher;

/**
 * JCESecurityModule is an implementation of a security module in software.
 * 

* It doesn't require any hardware device to work.
* JCESecurityModule also implements the SMAdapter, so you can view it: either * as a self contained security module adapter that doesn't need a security module * or a security module that plugs directly to jpos, so doesn't need * a separate adapter.
* It relies on Java(tm) Cryptography Extension (JCE), hence its name.
* JCESecurityModule relies on the JCEHandler class to do the low level JCE work. *

* WARNING: This version of JCESecurityModule is meant for testing purposes and * NOT for life operation, since the Local Master Keys are stored in CLEAR on * the system's disk. Comming versions of JCESecurity Module will rely on * java.security.KeyStore for a better protection of the Local Master Keys. * * @author Hani Samuel Kirollos * @author Robert Demski * @version $Revision$ $Date$ */ @SuppressWarnings("unchecked") public class JCESecurityModule extends BaseSMAdapter { /** * Pattern representing key type string value. */ private static final Pattern KEY_TYPE_PATTERN = Pattern.compile("([^:;]*)([:;])?([^:;])?([^:;])?"); /** * Pattern for split two clear pins. */ private static final Pattern SPLIT_PIN_PATTERN = Pattern.compile("[ :;,/.]"); /** * NUmber of LMK pairs */ private final static int LMK_PAIRS_NO = 0xe; /** * LMK variants appled to first byte of LMK pair */ private final static int[] variants = { 0x00, 0xa6, 0x5a, 0x6a, 0xde, 0x2b, 0x50, 0x74, 0x9c,0xfa }; /** * LMK scheme variants appiled to first byte of second key in pair */ private final static int[] schemeVariants = { 0x00, 0xa6, 0x5a, 0x6a, 0xde, 0x2b }; /** * Index of scheme variant for left LMK key for double length keys */ private final static int KEY_U_LEFT = 1; /** * Index of scheme variant for right LMK key for double length keys */ private final static int KEY_U_RIGHT = 2; /** * Index of scheme variant for left LMK key for triple length keys */ private final static int KEY_T_LEFT = 3; /** * Index of scheme variant for middle LMK key for triple length keys */ private final static int KEY_T_MEDIUM = 4; /** * Index of scheme variant for right LMK key for triple length keys */ private final static int KEY_T_RIGHT = 5; private static MessageDigest SHA1_MESSAGE_DIGEST = null; private static final byte[] _VARIANT_RIGHT_HALF = new byte[] { (byte) 0xC0, (byte) 0xC0, (byte) 0xC0, (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; static { try { SHA1_MESSAGE_DIGEST = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException ex) {} //NOPMD: SHA-1 is a standard digest } /** * Creates an uninitialized JCE Security Module, you need to setConfiguration to initialize it */ public JCESecurityModule () { super(); } /** * @param lmkFile Local Master Keys filename of the JCE Security Module * @throws SMException */ public JCESecurityModule (String lmkFile) throws SMException { Objects.requireNonNull(lmkFile); init(null, lmkFile, false); } public JCESecurityModule (String lmkFile, String jceProviderClassName) throws SMException { Objects.requireNonNull(lmkFile); init(jceProviderClassName, lmkFile, false); } public JCESecurityModule (Configuration cfg, Logger logger, String realm) throws ConfigurationException { setLogger(logger, realm); setConfiguration(cfg); } /** * Configures a JCESecurityModule * @param cfg The following properties are read:
* lmk: Local Master Keys file (The only required parameter)
* jce: JCE Provider Class Name, if not provided, it defaults to: com.sun.crypto.provider.SunJCE
* rebuildlmk: (true/false), rebuilds the Local Master Keys file with new keys (WARNING: old keys will be erased)
* cbc-mac: Cipher Block Chaining MAC algorithm name for given JCE Provider.
* Default is ISO9797ALG3MACWITHISO7816-4PADDING from BouncyCastle provider (known as Retail-MAC)
* that is suitable for most of interfaces with double length MAC key
* ANSI X9.19 aka ISO/IEC 9797-1 MAC algorithm 3 padding method 2 - ISO7816
* ede-mac: Encrypt Decrypt Encrypt MAC algorithm name for given JCE Provider.
* Default is DESEDEMAC from BouncyCastle provider
* that is suitable for BASE24 with double length MAC key
* ANSI X9.19
* @throws ConfigurationException */ @Override public void setConfiguration (Configuration cfg) throws ConfigurationException { this.cfg = cfg; try { init(cfg.get("provider"), cfg.get("lmk", null), cfg.getBoolean("rebuildlmk")); } catch (SMException e) { throw new ConfigurationException(e); } } @Override public SecureDESKey generateKeyImpl (short keyLength, String keyType) throws SMException { Key generatedClearKey = jceHandler.generateDESKey(keyLength); return encryptToLMK(keyLength, keyType, generatedClearKey); } @Override public SecureDESKey importKeyImpl (short keyLength, String keyType, byte[] encryptedKey, SecureDESKey kek, boolean checkParity) throws SMException { // decrypt encrypted key Key clearKEY = jceHandler.decryptDESKey(keyLength, encryptedKey, decryptFromLMK(kek), checkParity); // Encrypt Key under LMK return encryptToLMK(keyLength, keyType, clearKEY); } @Override public byte[] exportKeyImpl (SecureDESKey key, SecureDESKey kek) throws SMException { // get key in clear Key clearKey = decryptFromLMK(key); // Encrypt key under kek return jceHandler.encryptDESKey(key.getKeyLength(), clearKey, decryptFromLMK(kek)); } private int getKeyTypeIndex (short keyLength, String keyType) throws SMException { int index; if (keyType==null) return 0; String majorType = getMajorType(keyType); if (!keyTypeToLMKIndex.containsKey(majorType)) throw new SMException("Unsupported key type: " + majorType); index = keyTypeToLMKIndex.get(majorType); index |= getVariant(keyType) << 8; return index; } private static String getMajorType (String keyType) { Matcher m = KEY_TYPE_PATTERN.matcher(keyType); m.find(); if (m.group(1) != null) return m.group(1); throw new IllegalArgumentException("Missing key type"); } private static int getVariant (String keyType) { int variant = 0; Matcher m = KEY_TYPE_PATTERN.matcher(keyType); m.find(); if (m.group(3) != null) try { variant = Integer.valueOf(m.group(3)); } catch (NumberFormatException ex){ throw new NumberFormatException("Value "+m.group(4)+" is not valid key variant"); } return variant; } private static KeyScheme getScheme (int keyLength, String keyType) { KeyScheme scheme = KeyScheme.Z; switch (keyLength){ case SMAdapter.LENGTH_DES: scheme = KeyScheme.Z; break; case SMAdapter.LENGTH_DES3_2KEY: scheme = KeyScheme.X; break; case SMAdapter.LENGTH_DES3_3KEY: scheme = KeyScheme.Y; break; } if (keyType==null) return scheme; Matcher m = KEY_TYPE_PATTERN.matcher(keyType); m.find(); if (m.group(4) != null) try { scheme = KeyScheme.valueOf(m.group(4)); } catch (IllegalArgumentException ex){ throw new IllegalArgumentException("Value "+m.group(4)+" is not valid key scheme"); } return scheme; } @Override public EncryptedPIN encryptPINImpl (String pin, String accountNumber) throws SMException { byte[] clearPINBlock = calculatePINBlock(pin, FORMAT00, accountNumber); // Encrypt byte[] translatedPINBlock = jceHandler.encryptData(clearPINBlock, getLMK(PINLMKIndex)); return new EncryptedPIN(translatedPINBlock, FORMAT00, accountNumber, false); } @Override public String decryptPINImpl (EncryptedPIN pinUnderLmk) throws SMException { byte[] clearPINBlock = jceHandler.decryptData(pinUnderLmk.getPINBlock(), getLMK(PINLMKIndex)); return calculatePIN(clearPINBlock, pinUnderLmk.getPINBlockFormat(), pinUnderLmk.getAccountNumber()); } @Override public EncryptedPIN importPINImpl (EncryptedPIN pinUnderKd1, SecureDESKey kd1) throws SMException { // read inputs String accountNumber = pinUnderKd1.getAccountNumber(); // Use FORMAT00 for encrypting PIN under LMK byte destinationPINBlockFormat = FORMAT00; // get clear PIN byte[] clearPINBlock = jceHandler.decryptData(pinUnderKd1.getPINBlock(), decryptFromLMK(kd1)); // extract clear pin (as entered by card holder) String pin = calculatePIN(clearPINBlock, pinUnderKd1.getPINBlockFormat(), accountNumber); // Format PIN Block using proprietary FORMAT00 to be encrypetd under LMK clearPINBlock = calculatePINBlock(pin, destinationPINBlockFormat, accountNumber); // encrypt PIN byte[] translatedPINBlock = jceHandler.encryptData(clearPINBlock, getLMK(PINLMKIndex)); return new EncryptedPIN(translatedPINBlock, destinationPINBlockFormat, accountNumber, false); } @Override public EncryptedPIN exportPINImpl (EncryptedPIN pinUnderLmk, SecureDESKey kd2, byte destinationPINBlockFormat) throws SMException { String accountNumber = pinUnderLmk.getAccountNumber(); // process // get clear PIN byte[] clearPINBlock = jceHandler.decryptData(pinUnderLmk.getPINBlock(), getLMK(PINLMKIndex)); // extract clear pin String pin = calculatePIN(clearPINBlock, pinUnderLmk.getPINBlockFormat(), accountNumber); clearPINBlock = calculatePINBlock(pin, destinationPINBlockFormat, accountNumber); // encrypt PIN byte[] translatedPINBlock = jceHandler.encryptData(clearPINBlock, decryptFromLMK(kd2)); return new EncryptedPIN(translatedPINBlock, destinationPINBlockFormat, accountNumber, false); } @Override public EncryptedPIN generatePINImpl(String accountNumber, int pinLen, List excludes) throws SMException { if (excludes==null) excludes = Arrays.asList(); String pin; { Random rd = new SecureRandom(); int max = (int)Math.pow(10, Math.min(pinLen, 9)); int max2 = (int)Math.pow(10, Math.max(pinLen - 9,0)); do { long pinl = rd.nextInt(max); if (pinLen > 9){ pinl *= max2; pinl += rd.nextInt(max2); } pin = ISOUtil.zeropad(pinl, pinLen); } while (excludes.contains(pin)); } return encryptPINImpl(pin, accountNumber); } /** * Visa way to decimalize data block * @param b * @return decimalized data string */ private static String decimalizeVisa(byte[] b){ char[] bec = ISOUtil.hexString(b).toUpperCase().toCharArray(); char[] bhc = new char[bec.length]; int k = 0; //Select 0-9 chars for ( char c : bec ) if (c<'A') bhc[k++] = c; //Select A-F chars and map them to 0-5 char adjust = 'A'-'0'; for ( char c : bec ) if (c>='A') bhc[k++] = (char) (c-adjust); return new String(bhc); } protected Key concatKeys(SecureDESKey keyA, SecureDESKey keyB) throws SMException { if ( keyA!=null && keyA.getKeyLength()==SMAdapter.LENGTH_DES && keyB!=null && keyB.getKeyLength()==SMAdapter.LENGTH_DES) { Key cvkAclear = decryptFromLMK(keyA); Key cvkBclear = decryptFromLMK(keyB); return jceHandler.formDESKey(SMAdapter.LENGTH_DES3_2KEY ,ISOUtil.concat(cvkAclear.getEncoded(), cvkBclear.getEncoded())); } if (keyA!=null && keyA.getKeyLength()!=SMAdapter.LENGTH_DES) return decryptFromLMK(keyA); if (keyB!=null && keyB.getKeyLength()!=SMAdapter.LENGTH_DES) return decryptFromLMK(keyB); return null; } protected String calculateCVV(String accountNo, Key cvk, Date expDate, String serviceCode) throws SMException { String ed = ISODate.formatDate(expDate, "yyMM"); return calculateCVD(accountNo, cvk, ed, serviceCode); } protected String calculateCVD(String accountNo, Key cvk, String expDate, String serviceCode) throws SMException { Key udka = jceHandler.formDESKey(SMAdapter.LENGTH_DES ,Arrays.copyOfRange(cvk.getEncoded(), 0, 8)); byte[] block = ISOUtil.hex2byte( ISOUtil.zeropadRight(accountNo + expDate + serviceCode, 32)); byte[] ba = Arrays.copyOfRange(block, 0, 8); byte[] bb = Arrays.copyOfRange(block, 8,16); //Encrypt ba with udka byte[] bc = jceHandler.encryptData(ba, udka); byte[] bd = ISOUtil.xor(bc, bb); //Encrypt bd Tripple DES byte[] be = jceHandler.encryptData(bd, cvk); return decimalizeVisa(be).substring(0,3); } @Override protected String calculateCVVImpl(String accountNo, SecureDESKey cvkA, SecureDESKey cvkB, Date expDate, String serviceCode) throws SMException { return calculateCVV(accountNo,concatKeys(cvkA, cvkB),expDate,serviceCode); } @Override protected String calculateCVDImpl(String accountNo, SecureDESKey cvkA, SecureDESKey cvkB, String expDate, String serviceCode) throws SMException { return calculateCVD(accountNo, concatKeys(cvkA, cvkB), expDate, serviceCode); } protected void checkCAVVArgs(String upn, String authrc, String sfarc) throws SMException { if (upn == null) throw new SMException("Unpredictable Number can not be null"); if (authrc == null) throw new SMException("Authorization Result Code can not be null"); if (sfarc == null) throw new SMException("Secend Factor Authorization Result Code" + " can not be null"); if (upn.length() != 4 ) throw new SMException("Length of Unpredictable Number" + " must be 4 but got "+upn.length()); if (authrc.length() != 1 ) throw new SMException("Length of Authorization Result Code" + " must be 1 but got "+authrc.length()); if (sfarc.length() != 2 ) throw new SMException("Length of Secend Factor Authorization Result" + " Code must be 2 but got "+sfarc.length()); } @Override protected String calculateCAVVImpl(String accountNo, SecureDESKey cvk, String upn, String authrc, String sfarc) throws SMException { checkCAVVArgs(upn, authrc,sfarc); return calculateCVD(accountNo,concatKeys(cvk, null),upn,authrc+sfarc); } @Override protected boolean verifyCVVImpl(String accountNo, SecureDESKey cvkA, SecureDESKey cvkB, String cvv, Date expDate, String serviceCode) throws SMException { String result = calculateCVV(accountNo, concatKeys(cvkA, cvkB), expDate, serviceCode); return result.equals(cvv); } @Override protected boolean verifyCVVImpl(String accountNo, SecureDESKey cvkA, SecureDESKey cvkB, String cvv, String expDate, String serviceCode) throws SMException { String result = calculateCVD(accountNo, concatKeys(cvkA, cvkB), expDate, serviceCode); return result.equals(cvv); } @Override protected boolean verifyCAVVImpl(String accountNo, SecureDESKey cvk, String cavv, String upn, String authrc, String sfarc) throws SMException { checkCAVVArgs(upn, authrc,sfarc); String result = calculateCVD(accountNo, concatKeys(cvk, null), upn, authrc+sfarc); return result.equals(cavv); } protected String calculatedCVV(String accountNo, SecureDESKey imkac, String expDate, String serviceCode, byte[] atc, MKDMethod mkdm) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN_dCVD(accountNo, null, mkdm); Key mkac = deriveICCMasterKey(decryptFromLMK(imkac), panpsn); String alteredPAN = ISOUtil.hexString(atc) + accountNo.substring(4); return calculateCVD(alteredPAN, mkac, expDate, serviceCode); } @Override protected boolean verifydCVVImpl(String accountNo, SecureDESKey imkac, String dcvv, Date expDate, String serviceCode, byte[] atc, MKDMethod mkdm) throws SMException { String ed = ISODate.formatDate(expDate, "yyMM"); String res = calculatedCVV(accountNo, imkac, ed, serviceCode, atc, mkdm ); return res.equals(dcvv); } @Override protected boolean verifydCVVImpl(String accountNo, SecureDESKey imkac, String dcvv, String expDate, String serviceCode, byte[] atc, MKDMethod mkdm) throws SMException { String res = calculatedCVV(accountNo, imkac, expDate, serviceCode, atc, mkdm ); return res.equals(dcvv); } protected String calculateCVC3(SecureDESKey imkcvc3, String accountNo, String acctSeqNo, byte[] atc, byte[] upn, byte[] data, MKDMethod mkdm) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN_dCVD(accountNo, acctSeqNo, mkdm); Key mkcvc3 = deriveICCMasterKey(decryptFromLMK(imkcvc3), panpsn); byte[] ivcvc3 = data; if (ivcvc3.length != 2) //Compute IVCVC3 ivcvc3 = calculateIVCVC3(mkcvc3, data); //Concatenate IVCVC3, UPN and ATC byte[] b = ISOUtil.concat(ivcvc3, upn); b = ISOUtil.concat(b, atc); //Encrypt prepared blok b = jceHandler.encryptData(b, mkcvc3); //Format last two bytes to integer int cvc3l = (b[6] & 0xff) << 8; cvc3l |= b[7] & 0xff; //Convert to string representation return String.format("%05d",cvc3l); } @Override protected boolean verifyCVC3Impl(SecureDESKey imkcvc3, String accountNo, String acctSeqNo, byte[] atc, byte[] upn, byte[] data, MKDMethod mkdm, String cvc3) throws SMException { String calcCVC3 = calculateCVC3(imkcvc3, accountNo, acctSeqNo, atc, upn, data, mkdm ); cvc3 = cvc3==null?"":cvc3; //get some last digits calcCVC3 = calcCVC3.substring(5-cvc3.length()); return calcCVC3.equals(cvc3); } private byte[] calculateIVCVC3(Key mkcvc3, byte[] data) throws JCEHandlerException { byte[] paddedData = paddingISO9797Method2(data); byte[] mac = calculateMACISO9797Alg3(mkcvc3, paddedData); return Arrays.copyOfRange(mac,6,8); } /** * ISO/IEC 9797-1 padding method 2 * @param d da to be padded * @return padded data */ private byte[] paddingISO9797Method2(byte[] d) { //Padding - first byte 0x80 rest 0x00 byte[] t = new byte[d.length - d.length%8 + 8]; System.arraycopy(d, 0, t, 0, d.length); for (int i=d.length;i * Used at EMV operations: {@link #verifyARQCImpl ARQC verification}, * {@link #generateARPCImpl ARPC generation}, * {@link #generateSM_MACImpl secure message MAC generation} * and {@link #translatePINGenerateSM_MACImpl secure message PIN translation} */ private static byte[] formatPANPSN(String pan, String psn, MKDMethod mkdm) throws SMException { switch (mkdm){ case OPTION_A: return formatPANPSNOptionA(pan, psn); case OPTION_B: if ( pan.length() <= 16) //use OPTION_A return formatPANPSNOptionA(pan, psn); return formatPANPSNOptionB(pan, psn); default: throw new SMException("Unsupported ICC Master Key derivation method"); } } /** * Prepare 8-bytes data from PAN and PAN Sequence Number. *

* Used at dCVV verification {@link #verifydCVVImpl verifydCVV} * and CVC3 {@link #verifyCVC3Impl verifyCVC3} */ private static byte[] formatPANPSN_dCVD(String pan, String psn, MKDMethod mkdm) throws SMException { switch (mkdm){ case OPTION_A: return formatPANPSNOptionA(pan, psn); case OPTION_B: return formatPANPSNOptionB(pan, psn); default: throw new SMException("Unsupported ICC Master Key derivation method"); } } /** * Format bytes representing Application PAN and * PAN Sequence Number in BCD format. *

* Concatenate from left to right decimal digits of PAN and * PAN Sequence Number digits. If {@code psn} is not present, it is * replaced by a "00" digits. If the result is less than 16 digits long, * pad it to the left with hexadecimal zeros in order to obtain an * 16-digit number. If the Application PAN has an odd number of decimal * digits then concatenate a "0" padding digit to the left thereby * ensuring that the result is an even number of digits. * * @param pan application primary account number * @param psn PAN Sequence Number * @return up to 11 bytes representing Application PAN */ private static byte[] preparePANPSN(String pan, String psn){ if (psn == null || psn.isEmpty()) psn = "00"; String ret = pan + psn; //convert digits to bytes and padd with "0" //to left for ensure even number of digits return ISOUtil.hex2byte(ret); } /** * Prepare 8-bytes data from PAN and PAN Sequence Number (Option A) *

    *
  • Prepare Application PAN and PAN Sequence Number by {@see #preparePANPSN} *
  • Select first 16 digits *
* @param pan application primary account number * @param psn PAN Sequence Number * @return 8-bytes representing first 16 digits */ private static byte[] formatPANPSNOptionA(String pan, String psn){ if ( pan.length() < 14 ) try { pan = ISOUtil.zeropad(pan, 14); } catch( ISOException ex ) {} //NOPMD: ISOException condition is checked before. byte[] b = preparePANPSN(pan, psn); return Arrays.copyOfRange(b, b.length-8, b.length); } /** * Prepare bytes data from PAN and PAN Sequence Number (Option B) * *
Do the following:
*
    *
  • Prepare Application PAN and PAN Sequence Number by {@see #preparePANPSN} *
  • Hash the prepared result using the SHA-1 hashing algorithm * to obtain the 20-byte hash result *
  • Decimalize result of hasing by {@see #decimalizeVisa } *
  • Select first 16 decimal digits *
* @param pan application primary account number * @param psn PAN Sequence Number * @return 8-bytes representing first 16 decimalised digits */ private static byte[] formatPANPSNOptionB(String pan, String psn){ byte[] b = preparePANPSN(pan, psn); //20-bytes sha-1 digest byte[] r = SHA1_MESSAGE_DIGEST.digest(b); //decimalized HEX string of digest String rs = decimalizeVisa(r); //return 16-bytes decimalizd digest return ISOUtil.hex2byte(rs.substring(0,16)); } /** * Derive ICC Master Key from Issuer Master Key and preformated PAN/PANSeqNo * * Compute two 8-byte numbers: *
  • left part is a result of Tripple-DES encription {@code panpsn} * with {@code imk} as the key *
  • right part is a result of Tripple-DES binary inverted * {@code panpsn} with {@code imk} as the key *
  • concatenate left and right parts *
    * Described in EMV v4.2 Book 2, Annex A1.4.1 Master Key Derivation point 2 * * @param imk 16-bytes Issuer Master Key * @param panpsn preformated PAN and PAN Sequence Number * @return derived 16-bytes ICC Master Key with adjusted DES parity * @throws JCEHandlerException */ protected Key deriveICCMasterKey(Key imk, byte[] panpsn) throws JCEHandlerException { byte[] l = Arrays.copyOfRange(panpsn, 0, 8); //left part of derived key l = jceHandler.encryptData(l, imk); byte[] r = Arrays.copyOfRange(panpsn, 0, 8); //inverse clear right part of key r = ISOUtil.xor(r, JCESecurityModule.fPaddingBlock); //right part of derived key r = jceHandler.encryptData(r,imk); //derived key byte[] mk = ISOUtil.concat(l,r); //fix DES parity of key Util.adjustDESParity(mk); //form JCE Tripple-DES Key return jceHandler.formDESKey(SMAdapter.LENGTH_DES3_2KEY, mk); } protected String calculatePVV(EncryptedPIN pinUnderLmk, Key key, int keyIdx ,List excludes) throws SMException { String pin = decryptPINImpl(pinUnderLmk); if (excludes!=null && excludes.contains(pin)) throw new WeakPINException("Given PIN is on excludes list"); String block = pinUnderLmk.getAccountNumber().substring(1); block += Integer.toString(keyIdx%10); block += pin.substring(0, 4); byte[] b = ISOUtil.hex2byte(block); b = jceHandler.encryptData(b, key); return decimalizeVisa(b).substring(0,4); } @Override protected String calculatePVVImpl(EncryptedPIN pinUnderLmk, SecureDESKey pvkA, SecureDESKey pvkB, int pvkIdx, List excludes) throws SMException { Key key = concatKeys(pvkA, pvkB); return calculatePVV(pinUnderLmk, key, pvkIdx, excludes); } @Override protected String calculatePVVImpl(EncryptedPIN pinUnderKd1, SecureDESKey kd1, SecureDESKey pvkA, SecureDESKey pvkB, int pvkIdx, List excludes) throws SMException { Key key = concatKeys(pvkA, pvkB); EncryptedPIN pinUnderLmk = importPINImpl(pinUnderKd1, kd1); return calculatePVV(pinUnderLmk, key, pvkIdx, excludes); } @Override public boolean verifyPVVImpl(EncryptedPIN pinUnderKd1, SecureDESKey kd1, SecureDESKey pvkA, SecureDESKey pvkB, int pvki, String pvv) throws SMException { Key key = concatKeys(pvkA, pvkB); EncryptedPIN pinUnderLmk = importPINImpl(pinUnderKd1, kd1); String result = calculatePVV(pinUnderLmk, key, pvki, null); return result.equals(pvv); } @Override public EncryptedPIN translatePINImpl (EncryptedPIN pinUnderKd1, SecureDESKey kd1, SecureDESKey kd2, byte destinationPINBlockFormat) throws SMException { EncryptedPIN translatedPIN; Key clearKd1 = decryptFromLMK(kd1); Key clearKd2 = decryptFromLMK(kd2); translatedPIN = translatePINExt(null, pinUnderKd1, clearKd1, clearKd2 ,destinationPINBlockFormat, null, PaddingMethod.MCHIP); return translatedPIN; } private EncryptedPIN translatePINExt (EncryptedPIN oldPinUnderKd1, EncryptedPIN pinUnderKd1, Key kd1, Key kd2, byte destinationPINBlockFormat, Key udk, PaddingMethod padm) throws SMException { String accountNumber = pinUnderKd1.getAccountNumber(); // get clear PIN byte[] clearPINBlock = jceHandler.decryptData(pinUnderKd1.getPINBlock(), kd1); String pin = calculatePIN(clearPINBlock, pinUnderKd1.getPINBlockFormat(), accountNumber); // Reformat PIN Block byte[] translatedPINBlock; if (isVSDCPinBlockFormat(destinationPINBlockFormat)) { String udka = ISOUtil.hexString(Arrays.copyOfRange(udk.getEncoded(), 0, 8)); if (destinationPINBlockFormat == SMAdapter.FORMAT42 ) { byte[] oldClearPINBlock = jceHandler.decryptData(oldPinUnderKd1.getPINBlock(), kd1); String oldPIN = calculatePIN(oldClearPINBlock, oldPinUnderKd1.getPINBlockFormat(), accountNumber); clearPINBlock = calculatePINBlock(pin+":"+oldPIN, destinationPINBlockFormat, udka); } else clearPINBlock = calculatePINBlock(pin, destinationPINBlockFormat, udka); accountNumber = udka.substring(4); } else { clearPINBlock = calculatePINBlock(pin, destinationPINBlockFormat, accountNumber); } switch(padm){ case VSDC: //Add VSDC pin block padding clearPINBlock = ISOUtil.concat(new byte[]{0x08}, clearPINBlock); clearPINBlock = paddingISO9797Method2(clearPINBlock); break; case CCD: //Add CCD pin block padding clearPINBlock = paddingISO9797Method2(clearPINBlock); break; default: } // encrypt PIN if (padm == PaddingMethod.CCD) translatedPINBlock = jceHandler.encryptDataCBC(clearPINBlock, kd2 ,Arrays.copyOf(zeroBlock, zeroBlock.length)); else translatedPINBlock = jceHandler.encryptData(clearPINBlock, kd2); return new EncryptedPIN(translatedPINBlock ,destinationPINBlockFormat, accountNumber, false); } /** * Derive the session key used for Application Cryptogram verification * or for secure messaging, the diversification value is the ATC * @param mkac unique ICC Master Key for Application Cryptogams or Secure Messaging * @param atc ICC generated Application Transaction Counter as diversification value * @return derived 16-bytes Session Key with adjusted DES parity * @throws JCEHandlerException */ private Key deriveSK_VISA(Key mkac, byte[] atc) throws JCEHandlerException { byte[] skl = new byte[8]; System.arraycopy(atc, atc.length-2, skl, 6, 2); skl = ISOUtil.xor(skl, Arrays.copyOfRange(mkac.getEncoded(),0 ,8)); byte[] skr = new byte[8]; System.arraycopy(atc, atc.length-2, skr, 6, 2); skr = ISOUtil.xor(skr, ISOUtil.hex2byte("000000000000FFFF")); skr = ISOUtil.xor(skr, Arrays.copyOfRange(mkac.getEncoded(),8 ,16)); Util.adjustDESParity(skl); Util.adjustDESParity(skr); return jceHandler.formDESKey(SMAdapter.LENGTH_DES3_2KEY, ISOUtil.concat(skl,skr)); } /** * Common Session Key Derivation Method for secure messaging. *

    * The diversification value is the RAND, which is ARQC * incremeted by 1 (with overflow) after each script command * for that same ATC value. * Described in EMV v4.2 Book 2, Annex A1.3.1 Common Session Key * Derivation Option for secure messaging. * * @param mksm unique ICC Master Key for Secure Messaging * @param rand Application Cryptogram as diversification value * @return derived 16-bytes Session Key with adjusted DES parity * @throws JCEHandlerException */ private Key deriveCommonSK_SM(Key mksm, byte[] rand) throws JCEHandlerException { byte[] rl = Arrays.copyOf(rand,8); rl[2] = (byte)0xf0; byte[] skl = jceHandler.encryptData(rl, mksm); byte[] rr = Arrays.copyOf(rand,8); rr[2] = (byte)0x0f; byte[] skr = jceHandler.encryptData(rr, mksm); Util.adjustDESParity(skl); Util.adjustDESParity(skr); return jceHandler.formDESKey(SMAdapter.LENGTH_DES3_2KEY, ISOUtil.concat(skl,skr)); } /** * Common Session Key Derivation Method for Application Cryptograms. *

    * Described in EMV v4.2 Book 2, Annex A1.3.1 Common Session Key Derivation * Option for Application Cryptograms. * * @param mkac unique ICC Master Key for Application Cryptogams. * @param atc ICC generated Application Transaction Counter as diversification value. * @return derived 16-bytes Session Key with adjusted DES parity. * @throws JCEHandlerException */ private Key deriveCommonSK_AC(Key mkac, byte[] atc) throws JCEHandlerException { byte[] r = new byte[8]; System.arraycopy(atc, atc.length-2, r, 0, 2); return deriveCommonSK_SM(mkac, r); } /** * MasterCard Proprietary Session Key Derivation (SKD) method. *

    * Described in M/Chip 4 version 1.1 Security & Key Management manual * paragraph 7 ICC Session Key Derivation. * * @param mkac unique ICC Master Key for Application Cryptogams * @param atc ICC generated Application Transaction Counter as diversification value * @param upn terminal generated random as diversification value * @return derived 16-bytes Session Key with adjusted DES parity * @throws JCEHandlerException */ private Key deriveSK_MK(Key mkac, byte[] atc, byte[] upn) throws JCEHandlerException { byte[] r = new byte[8]; System.arraycopy(atc, atc.length-2, r, 0, 2); System.arraycopy(upn, upn.length-4, r, 4, 4); return deriveCommonSK_SM(mkac, r); } private void constraintMKDM(MKDMethod mkdm, SKDMethod skdm) throws SMException { if (mkdm == MKDMethod.OPTION_B) throw new SMException("Master Key Derivation Option B" + " is not used in practice with scheme "+skdm); } private void constraintARPCM(SKDMethod skdm, ARPCMethod arpcMethod) throws SMException { if (arpcMethod == ARPCMethod.METHOD_2 ) throw new SMException("ARPC generation method 2" + " is not used in practice with scheme "+skdm); } /** * Calculate ARQC. *

    * Entry point e.g. for simulator systems */ protected byte[] calculateARQC(MKDMethod mkdm, SKDMethod skdm ,SecureDESKey imkac, String accountNo, String accntSeqNo, byte[] atc ,byte[] upn, byte[] transData) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN(accountNo, accntSeqNo, mkdm); Key mkac = deriveICCMasterKey(decryptFromLMK(imkac), panpsn); Key skac = mkac; switch(skdm){ case VSDC: constraintMKDM(mkdm, skdm); break; case MCHIP: constraintMKDM(mkdm, skdm); skac = deriveSK_MK(mkac,atc,upn); break; case EMV_CSKD: skac = deriveCommonSK_AC(mkac, atc); break; default: throw new SMException("Session Key Derivation "+skdm+" not supported"); } return calculateMACISO9797Alg3(skac, transData); } @Override protected boolean verifyARQCImpl(MKDMethod mkdm, SKDMethod skdm, SecureDESKey imkac ,String accountNo, String accntSeqNo, byte[] arqc, byte[] atc ,byte[] upn, byte[] transData) throws SMException { byte[] res = calculateARQC(mkdm, skdm, imkac, accountNo, accntSeqNo ,atc, upn, transData); return Arrays.equals(arqc, res); } @Override public byte[] generateARPCImpl(MKDMethod mkdm, SKDMethod skdm, SecureDESKey imkac ,String accountNo, String accntSeqNo, byte[] arqc, byte[] atc, byte[] upn ,ARPCMethod arpcMethod, byte[] arc, byte[] propAuthData) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN(accountNo, accntSeqNo, mkdm); Key mkac = deriveICCMasterKey(decryptFromLMK(imkac), panpsn); Key skarpc = mkac; switch(skdm){ case VSDC: constraintMKDM(mkdm, skdm); constraintARPCM(skdm, arpcMethod); break; case MCHIP: constraintMKDM(mkdm, skdm); constraintARPCM(skdm, arpcMethod); break; case EMV_CSKD: skarpc = deriveCommonSK_AC(mkac, atc); break; default: throw new SMException("Session Key Derivation "+skdm+" not supported"); } return calculateARPC(skarpc, arqc, arpcMethod, arc, propAuthData); } @Override public byte[] verifyARQCGenerateARPCImpl(MKDMethod mkdm, SKDMethod skdm, SecureDESKey imkac ,String accountNo, String accntSeqNo, byte[] arqc, byte[] atc, byte[] upn ,byte[] transData, ARPCMethod arpcMethod, byte[] arc, byte[] propAuthData) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN(accountNo, accntSeqNo, mkdm); Key mkac = deriveICCMasterKey(decryptFromLMK(imkac), panpsn); Key skac = mkac; Key skarpc = mkac; switch(skdm){ case VSDC: constraintMKDM(mkdm, skdm); constraintARPCM(skdm, arpcMethod); break; case MCHIP: constraintMKDM(mkdm, skdm); constraintARPCM(skdm, arpcMethod); skac = deriveSK_MK(mkac,atc,upn); break; case EMV_CSKD: skac = deriveSK_MK(mkac, atc, new byte[4]); skarpc = skac; break; default: throw new SMException("Session Key Derivation "+skdm+" not supported"); } if (!Arrays.equals(arqc, calculateMACISO9797Alg3(skac, transData))) return null; return calculateARPC(skarpc, arqc, arpcMethod, arc, propAuthData); } /** * Calculate ARPC. *

    * Entry point e.g. for simulator systems */ protected byte[] calculateARPC(Key skarpc, byte[] arqc, ARPCMethod arpcMethod ,byte[] arc, byte[] propAuthData) throws SMException { if (arpcMethod == null) arpcMethod = ARPCMethod.METHOD_1; byte[] b = new byte[8]; switch(arpcMethod) { case METHOD_1: System.arraycopy(arc, arc.length-2, b, 0, 2); b = ISOUtil.xor(arqc, b); return jceHandler.encryptData(b, skarpc); case METHOD_2: b = ISOUtil.concat(arqc, arc); if (propAuthData != null) b = ISOUtil.concat(b, propAuthData); b = paddingISO9797Method2(b); b = calculateMACISO9797Alg3(skarpc, b); return Arrays.copyOf(b, 4); default: throw new SMException("ARPC Method "+arpcMethod+" not supported"); } } @Override protected byte[] generateSM_MACImpl(MKDMethod mkdm, SKDMethod skdm ,SecureDESKey imksmi, String accountNo, String accntSeqNo ,byte[] atc, byte[] arqc, byte[] data) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN(accountNo, accntSeqNo, mkdm); Key mksmi = deriveICCMasterKey(decryptFromLMK(imksmi), panpsn); Key smi; switch(skdm){ case VSDC: smi = deriveSK_VISA(mksmi, atc); data = paddingISO9797Method2(data); break; case MCHIP: case EMV_CSKD: smi = deriveCommonSK_SM(mksmi,arqc); data = paddingISO9797Method2(data); break; default: throw new SMException("Session Key Derivation "+skdm+" not supported"); } return calculateMACISO9797Alg3(smi, data); } @Override protected Pair translatePINGenerateSM_MACImpl( MKDMethod mkdm, SKDMethod skdm, PaddingMethod padm, SecureDESKey imksmi ,String accountNo, String accntSeqNo, byte[] atc, byte[] arqc ,byte[] data, EncryptedPIN currentPIN, EncryptedPIN newPIN ,SecureDESKey kd1, SecureDESKey imksmc, SecureDESKey imkac ,byte destinationPINBlockFormat) throws SMException { if (mkdm==null) mkdm = MKDMethod.OPTION_A; byte[] panpsn = formatPANPSN(accountNo, accntSeqNo, mkdm); Key mksmc = deriveICCMasterKey(decryptFromLMK(imksmc), panpsn); Key smc; PaddingMethod derivedPADM; switch(skdm){ case VSDC: smc = deriveSK_VISA(mksmc, atc); derivedPADM = PaddingMethod.VSDC; break; case MCHIP: smc = deriveCommonSK_SM(mksmc,arqc); derivedPADM = PaddingMethod.MCHIP; break; case EMV_CSKD: smc = deriveCommonSK_SM(mksmc,arqc); derivedPADM = PaddingMethod.CCD; break; default: throw new SMException("Session Key Derivation "+skdm+" not supported"); } //Use derived Padding Method if not specified if ( padm == null ) padm = derivedPADM; Key udk = null; if (isVSDCPinBlockFormat(destinationPINBlockFormat)) udk = deriveICCMasterKey(decryptFromLMK(imkac), panpsn); EncryptedPIN pin = translatePINExt(currentPIN, newPIN, decryptFromLMK(kd1) ,smc, destinationPINBlockFormat, udk, padm); data = ISOUtil.concat(data, pin.getPINBlock()); byte[] mac = generateSM_MACImpl(mkdm, skdm, imksmi, accountNo ,accntSeqNo, atc, arqc, data); return new Pair(pin, mac); } private boolean isVSDCPinBlockFormat(byte pinBlockFormat) { return pinBlockFormat==SMAdapter.FORMAT41 || pinBlockFormat==SMAdapter.FORMAT42; } @Override public byte[] encryptDataImpl(CipherMode cipherMode, SecureDESKey kd ,byte[] data, byte[] iv) throws SMException { Key dek = decryptFromLMK(kd); return jceHandler.doCryptStuff(data, dek, Cipher.ENCRYPT_MODE, cipherMode, iv); } @Override public byte[] decryptDataImpl(CipherMode cipherMode, SecureDESKey kd ,byte[] data, byte[] iv) throws SMException { Key dek = decryptFromLMK(kd); return jceHandler.doCryptStuff(data, dek, Cipher.DECRYPT_MODE, cipherMode, iv); } /** * Generates CBC-MAC (Cipher Block Chaining Message Authentication Code) * for some data. * * @param data the data to be MACed * @param kd the key used for MACing * @return generated CBC-MAC bytes * @throws SMException */ @Override protected byte[] generateCBC_MACImpl (byte[] data, SecureDESKey kd) throws SMException { LogEvent evt = new LogEvent(this, "jce-provider-cbc-mac"); try { return generateMACImpl(data,kd,cfg.get("cbc-mac","ISO9797ALG3MACWITHISO7816-4PADDING"),evt); } catch (Exception e) { Logger.log(evt); throw e instanceof SMException ? (SMException)e : new SMException(e); } } /** * Generates EDE-MAC (Encrypt Decrypt Encrypt Message Authentication Code) * for some data. * * @param data the data to be MACed * @param kd the key used for MACing * @return generated EDE-MAC bytes * @throws SMException */ @Override protected byte[] generateEDE_MACImpl (byte[] data, SecureDESKey kd) throws SMException { LogEvent evt = new LogEvent(this, "jce-provider-ede-mac"); try { return generateMACImpl(data,kd,cfg.get("ede-mac","DESEDEMAC"),evt); } catch (Exception e) { Logger.log(evt); throw e instanceof SMException ? (SMException)e : new SMException(e); } } private byte[] generateMACImpl (byte[] data, SecureDESKey kd, String macAlgorithm, LogEvent evt) throws SMException { try { return jceHandler.generateMAC(data,decryptFromLMK(kd),macAlgorithm); } catch (JCEHandlerException e){ evt.addMessage(e); if (e.getCause() instanceof InvalidKeyException) throw new SMException(e); else throw new SMException("Unable to load MAC algorithm whose name is: " + macAlgorithm + ". Check that is used correct JCE provider and/or it is proper configured for this module.",e); } } /** * Generates a random clear key component. * @param keyLength * @return clear key componenet * @throws SMException */ public String generateClearKeyComponent (short keyLength) throws SMException { String clearKeyComponenetHexString; SimpleMsg[] cmdParameters = { new SimpleMsg("parameter", "Key Length", keyLength) }; LogEvent evt = new LogEvent(this, "s-m-operation"); evt.addMessage(new SimpleMsg("command", "Generate Clear Key Component", cmdParameters)); try { Key clearKey = jceHandler.generateDESKey(keyLength); byte[] clearKeyData = jceHandler.extractDESKeyMaterial(keyLength, clearKey); clearKeyComponenetHexString = ISOUtil.hexString(clearKeyData); evt.addMessage(new SimpleMsg("result", "Generated Clear Key Componenet", clearKeyComponenetHexString)); } catch (JCEHandlerException e) { evt.addMessage(e); throw e; } finally { Logger.log(evt); } return clearKeyComponenetHexString; } /** * Generates key check value.
    * @param secureDESKey SecureDESKey with untrusted or fake Key Check Value * @return generated Key Check Value * @throws SMException */ @Override protected byte[] generateKeyCheckValueImpl (SecureDESKey secureDESKey) throws SMException { return calculateKeyCheckValue(decryptFromLMK(secureDESKey)); } @Override public SecureDESKey translateKeySchemeImpl (SecureDESKey key, KeyScheme keyScheme) throws SMException { if (key == null) throw new SMException("Missing key to change"); if (keyScheme == null) throw new SMException("Missing desired key schame"); if (keyScheme == key.getScheme()) return key; switch (key.getScheme()) { case Z: throw new SMException("Single length DES key using the variant method" + " can not be converted to any other key"); case U: case T: throw new SMException("DES key using the variant method can not " + "be converted to less secure key using X9.17 method"); case X: if (keyScheme != KeyScheme.U) throw new SMException("Double length key using X9.17 method may be " + "converted only to double length key using variant method"); break; case Y: if (keyScheme != KeyScheme.T) throw new SMException("Triple length key using X9.17 method may be " + "converted only to triple length key using variant method"); break; default: throw new SMException("Change key scheme allowed only for keys" + "using variant method"); } // get key in clear Key clearKey = decryptFromLMK(key); // Encrypt key under LMK String keyType = getMajorType(key.getKeyType()) + ":" + key.getVariant() + keyScheme; return encryptToLMK(key.getKeyLength(), keyType, clearKey); } /** * Forms a key from 3 clear components and returns it encrypted under its corresponding LMK * The corresponding LMK is determined from the keyType * @param keyLength e.g. LENGTH_DES, LENGTH_DES3_2, LENGTH_DES3_3, .. * @param keyType possible values are those defined in the SecurityModule inteface. e.g., ZMK, TMK,... * @param clearComponent1HexString HexString containing the first component * @param clearComponent2HexString HexString containing the second component * @param clearComponent3HexString HexString containing the second component * @return forms an SecureDESKey from two clear components * @throws SMException */ public SecureDESKey formKEYfromThreeClearComponents( short keyLength, String keyType, String clearComponent1HexString, String clearComponent2HexString, String clearComponent3HexString) throws SMException { SecureDESKey secureDESKey; LogEvent evt = new LogEvent(this, "s-m-operation"); try { byte[] clearComponent1 = ISOUtil.hex2byte(clearComponent1HexString); byte[] clearComponent2 = clearComponent2HexString != null ? ISOUtil.hex2byte(clearComponent2HexString) : new byte[keyLength >> 3]; byte[] clearComponent3 = clearComponent3HexString != null ? ISOUtil.hex2byte(clearComponent3HexString) : new byte[keyLength >> 3]; byte[] clearKeyBytes = ISOUtil.xor(ISOUtil.xor(clearComponent1, clearComponent2), clearComponent3); Key clearKey = jceHandler.formDESKey(keyLength, clearKeyBytes); secureDESKey = encryptToLMK(keyLength, keyType, clearKey); SimpleMsg[] cmdParameters = { new SimpleMsg("parameter", "Key Length", keyLength), new SimpleMsg("parameter", "Key Type", keyType), new SimpleMsg("parameter", "Component 1 Check Value", calculateKeyCheckValue(jceHandler.formDESKey(keyLength, clearComponent1))), new SimpleMsg("parameter", "Component 2 Check Value", calculateKeyCheckValue(jceHandler.formDESKey(keyLength, clearComponent2))), new SimpleMsg("parameter", "Component 3 Check Value", calculateKeyCheckValue(jceHandler.formDESKey(keyLength, clearComponent3))) }; evt.addMessage(new SimpleMsg("command", "Form Key from Three Clear Components", cmdParameters)); evt.addMessage(new SimpleMsg("result", "Formed Key", secureDESKey)); } catch (JCEHandlerException e) { evt.addMessage(e); throw e; } finally { Logger.log(evt); } return secureDESKey; } @Override public SecureDESKey formKEYfromClearComponents (short keyLength, String keyType, String... components) throws SMException { if (components.length < 1 || components.length > 3) throw new SMException("Invalid number of clear key components"); String[] s = new String[3]; int i=0; for (String component : components) s[i++] = component; return formKEYfromThreeClearComponents(keyLength, keyType, s[0], s[1], s[2]); } /** * Calculates a key check value over a clear key * @param key * @return the key check value * @exception SMException */ protected byte[] calculateKeyCheckValue (Key key) throws SMException { byte[] encryptedZeroBlock = jceHandler.encryptData(zeroBlock, key); return ISOUtil.trim(encryptedZeroBlock, 3); } /** * Encrypts a clear DES Key under LMK to form a SecureKey * @param keyLength * @param keyType * @param clearDESKey * @return secureDESKey * @throws SMException */ protected SecureDESKey encryptToLMK (short keyLength, String keyType, Key clearDESKey) throws SMException { Key novar, left, medium, right; byte[] clearKeyBytes = jceHandler.extractDESKeyMaterial(keyLength, clearDESKey); byte[] bl = new byte[SMAdapter.LENGTH_DES>>3]; byte[] bm = new byte[SMAdapter.LENGTH_DES>>3]; byte[] br = new byte[SMAdapter.LENGTH_DES>>3]; byte[] encrypted = null; // enforce correct (odd) parity before encrypting the key Util.adjustDESParity(clearKeyBytes); int lmkIndex = getKeyTypeIndex(keyLength, keyType); switch ( getScheme(keyLength, keyType) ){ case Z: case X: case Y: novar = getLMK( lmkIndex ); encrypted = jceHandler.encryptData(clearKeyBytes, novar); break; case U: left = getLMK( KEY_U_LEFT << 12 | lmkIndex & 0xfff ); right = getLMK( KEY_U_RIGHT << 12 | lmkIndex & 0xfff ); System.arraycopy(clearKeyBytes, 0, bl, 0, bl.length); System.arraycopy(clearKeyBytes, bl.length, br, 0, br.length); bl = jceHandler.encryptData(bl, left); br = jceHandler.encryptData(br, right); encrypted = ISOUtil.concat(bl, br); break; case T: left = getLMK( KEY_T_LEFT << 12 | lmkIndex & 0xfff ); medium= getLMK( KEY_T_MEDIUM << 12 | lmkIndex & 0xfff ); right = getLMK( KEY_T_RIGHT << 12 | lmkIndex & 0xfff ); System.arraycopy(clearKeyBytes, 0, bl, 0, bl.length); System.arraycopy(clearKeyBytes, bl.length, bm, 0, bm.length); System.arraycopy(clearKeyBytes, bl.length+bm.length, br, 0, br.length); bl = jceHandler.encryptData(bl, left); bm = jceHandler.encryptData(bm, medium); br = jceHandler.encryptData(br, right); encrypted = ISOUtil.concat(bl, bm); encrypted = ISOUtil.concat(encrypted, br); break; } SecureDESKey secureDESKey = new SecureDESKey(keyLength, keyType, encrypted, calculateKeyCheckValue(clearDESKey)); return secureDESKey; } /** * Decrypts a secure DES key from encryption under LMK * @param secureDESKey (Key under LMK) * @return clear key * @throws SMException */ protected Key decryptFromLMK (SecureDESKey secureDESKey) throws SMException { Key left, medium, right; byte[] keyBytes = secureDESKey.getKeyBytes(); byte[] bl = new byte[SMAdapter.LENGTH_DES>>3]; byte[] bm = new byte[SMAdapter.LENGTH_DES>>3]; byte[] br = new byte[SMAdapter.LENGTH_DES>>3]; byte[] clearKeyBytes = null; Integer lmkIndex = getKeyTypeIndex(secureDESKey.getKeyLength(), secureDESKey.getKeyType()); if (lmkIndex==null) throw new SMException("Unsupported key type: " + secureDESKey.getKeyType()); lmkIndex |= secureDESKey.getVariant()<<8; switch ( secureDESKey.getScheme() ) { case Z: case X: case Y: clearKeyBytes = jceHandler.decryptData(keyBytes, getLMK( lmkIndex )); break; case U: left = getLMK( KEY_U_LEFT << 12 | lmkIndex & 0xfff ); right = getLMK( KEY_U_RIGHT << 12 | lmkIndex & 0xfff ); System.arraycopy(keyBytes, 0, bl, 0, bl.length); System.arraycopy(keyBytes, bl.length, br, 0, br.length); bl = jceHandler.decryptData(bl, left); br = jceHandler.decryptData(br, right); clearKeyBytes = ISOUtil.concat(bl, br); clearKeyBytes = ISOUtil.concat(clearKeyBytes, 0, clearKeyBytes.length, clearKeyBytes, 0, br.length ); break; case T: left = getLMK( KEY_T_LEFT << 12 | lmkIndex & 0xfff ); medium= getLMK( KEY_T_MEDIUM << 12 | lmkIndex & 0xfff ); right = getLMK( KEY_T_RIGHT << 12 | lmkIndex & 0xfff ); System.arraycopy(keyBytes, 0, bl, 0, bl.length); System.arraycopy(keyBytes, bl.length, bm, 0, bm.length); System.arraycopy(keyBytes, bl.length+bm.length, br, 0, br.length); bl = jceHandler.decryptData(bl, left); bm = jceHandler.decryptData(bm, medium); br = jceHandler.decryptData(br, right); clearKeyBytes = ISOUtil.concat(bl, bm); clearKeyBytes = ISOUtil.concat(clearKeyBytes, br); break; } if (!Util.isDESParityAdjusted(clearKeyBytes)) throw new JCEHandlerException("Parity not adjusted"); return jceHandler.formDESKey(secureDESKey.getKeyLength(), clearKeyBytes); } private char[] formatPINBlock(String pin, int checkDigit){ char[] block = ISOUtil.hexString(fPaddingBlock).toCharArray(); char[] pinLenHex = String.format("%02X", pin.length()).toCharArray(); pinLenHex[0] = (char)('0' + checkDigit); // pin length then pad with 'F' System.arraycopy(pinLenHex, 0, block, 0, pinLenHex.length); System.arraycopy(pin.toCharArray(), 0 ,block, pinLenHex.length, pin.length()); return block; } private String[] splitPins(String pins) { String[] pin = new String[2]; String[] p = SPLIT_PIN_PATTERN.split(pins); pin[0] = p[0]; if (p.length >= 2) pin[1] = p[1]; return pin; } /** * Calculates the clear PIN Block * @param pin as entered by the card holder on the PIN entry device * @param pinBlockFormat * @param accountNumber (the 12 right-most digits of the account number excluding the check digit) * @return The clear PIN Block * @throws SMException * */ protected byte[] calculatePINBlock (String pin, byte pinBlockFormat, String accountNumber) throws SMException { byte[] pinBlock = null; String oldPin = null; if (pinBlockFormat==SMAdapter.FORMAT42){ String[] p = splitPins(pin); pin = p[0]; oldPin = p[1]; if (oldPin.length() < MIN_PIN_LENGTH || oldPin.length() > MAX_PIN_LENGTH) throw new SMException("Invalid OLD PIN length: " + oldPin.length()); if (!ISOUtil.isNumeric(oldPin, 10)) throw new SMException("Invalid OLD PIN decimal digits: " + oldPin); } if (pin.length() < MIN_PIN_LENGTH || pin.length() > MAX_PIN_LENGTH) throw new SMException("Invalid PIN length: " + pin.length()); if (!ISOUtil.isNumeric(pin, 10)) throw new SMException("Invalid PIN decimal digits: " + pin); if (isVSDCPinBlockFormat(pinBlockFormat)) { if (accountNumber.length() != 16 ) throw new SMException("Invalid UDK-A: " + accountNumber + ". The length of the UDK-A must be 16 hexadecimal digits"); } else if (accountNumber.length() != 12) throw new SMException("Invalid Account Number: " + accountNumber + ". The length of the account number must be 12 (the 12 right-most digits of the account number excluding the check digit)"); switch (pinBlockFormat) { case FORMAT00: // same as FORMAT01 case FORMAT01: { // Block 1 byte[] block1 = ISOUtil.hex2byte(new String(formatPINBlock(pin,0x0))); // Block 2 byte[] block2 = ISOUtil.hex2byte("0000" + accountNumber); // pinBlock pinBlock = ISOUtil.xor(block1, block2); } break; case FORMAT03: { char[] block = ISOUtil.hexString(fPaddingBlock).toCharArray(); System.arraycopy(pin.toCharArray(), 0 ,block, 0, pin.length()); pinBlock = ISOUtil.hex2byte (new String(block)); } break; case FORMAT05: { // Block 1 char[] block1 = formatPINBlock(pin, 0x1); // Block rnd byte[] rnd = new byte[8]; Random rd = new SecureRandom(); rd.nextBytes(rnd); // Block 2 char[] block2 = ISOUtil.hexString(rnd).toCharArray(); // merge blocks System.arraycopy(block1, 0 ,block2, 0, pin.length() + 2 ); // pinBlock pinBlock = ISOUtil.hex2byte(new String(block2)); } break; case FORMAT34: { pinBlock = ISOUtil.hex2byte (new String(formatPINBlock(pin,0x2))); } break; case FORMAT35: { // Block 1 byte[] block1 = ISOUtil.hex2byte(new String(formatPINBlock(pin,0x2))); // Block 2 byte[] block2 = ISOUtil.hex2byte("0000" + accountNumber); // pinBlock pinBlock = ISOUtil.xor(block1, block2); } break; case FORMAT41: { // Block 1 byte[] block1 = ISOUtil.hex2byte(new String(formatPINBlock(pin,0x0))); // Block 2 - account number should contain Unique DEA Key A (UDK-A) byte[] block2 = ISOUtil.hex2byte("00000000" + accountNumber.substring(accountNumber.length()-8) ); // pinBlock pinBlock = ISOUtil.xor(block1, block2); } break; case FORMAT42: { // Block 1 byte[] block1 = ISOUtil.hex2byte(new String(formatPINBlock(pin,0x0))); // Block 2 - account number should contain Unique DEA Key A (UDK-A) byte[] block2 = ISOUtil.hex2byte("00000000" + accountNumber.substring(accountNumber.length()-8) ); // Block 3 - old pin byte[] block3 = ISOUtil.hex2byte(ISOUtil.zeropadRight(oldPin, 16)); // pinBlock pinBlock = ISOUtil.xor(block1, block2); pinBlock = ISOUtil.xor(pinBlock, block3); } break; default: throw new SMException("Unsupported PIN format: " + pinBlockFormat); } return pinBlock; } private void validatePinBlock(char[] pblock, int checkDigit, int padidx, int offset) throws SMException { validatePinBlock(pblock, checkDigit, padidx, offset, 'F'); } private void validatePinBlock(char[] pblock, int checkDigit ,int padidx, int offset, char padDigit) throws SMException { // test pin block check digit if (checkDigit >= 0 && pblock[0] - '0' != checkDigit) throw new SMException("PIN Block Error - invalid check digit"); // test pin block pdding int i = pblock.length - 1; while (i >= padidx) if (pblock[i--] != padDigit && padDigit > 0) throw new SMException("PIN Block Error - invalid padding"); // test pin block digits while (i >= offset) if (pblock[i--] >= 'A') throw new SMException("PIN Block Error - illegal pin digit"); // test pin length int pinLength = padidx - offset; if (pinLength < MIN_PIN_LENGTH || pinLength > MAX_PIN_LENGTH) throw new SMException("PIN Block Error - invalid pin length: " + pinLength); } /** * Calculates the clear pin (as entered by card holder on the pin entry device) * givin the clear PIN block * @param pinBlock clear PIN Block * @param pinBlockFormat * @param accountNumber * @return the pin * @throws SMException */ protected String calculatePIN (byte[] pinBlock, byte pinBlockFormat, String accountNumber) throws SMException { String pin = null; if (isVSDCPinBlockFormat(pinBlockFormat)) { if (accountNumber.length() != 16 ) throw new SMException("Invalid UDK-A: " + accountNumber + ". The length of the UDK-A must be 16 hexadecimal digits"); } else if (accountNumber.length() != 12) throw new SMException("Invalid Account Number: " + accountNumber + ". The length of the account number must be 12 (the 12 right-most digits of the account number excluding the check digit)"); switch (pinBlockFormat) { case FORMAT00: // same as format 01 case FORMAT01: { // Block 2 byte[] bl2 = ISOUtil.hex2byte("0000" + accountNumber); // get Block1 byte[] bl1 = ISOUtil.xor(pinBlock, bl2); int pinLength = bl1[0] & 0x0f; char[] block1 = ISOUtil.hexString(bl1).toCharArray(); int offset = 2; int checkDigit = 0x0; int padidx = pinLength + offset; // test pin block validatePinBlock(block1,checkDigit,padidx,offset); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT03: { String bl1 = ISOUtil.hexString(pinBlock); int padidx = bl1.indexOf('F'); if ( padidx < 0) padidx = 12; char[] block1 = bl1.toCharArray(); int checkDigit = -0x1; int offset = 0; // test pin block validatePinBlock(block1,checkDigit,padidx,offset); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT05: { // get Block1 byte[] bl1 = pinBlock; int pinLength = bl1[0] & 0x0f; char[] block1 = ISOUtil.hexString(bl1).toCharArray(); int offset = 2; int checkDigit = 0x01; int padidx = pinLength + offset; // test pin block validatePinBlock(block1, checkDigit, padidx, offset, (char) 0); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT34: { int pinLength = pinBlock[0] & 0x0f; char[] block1 = ISOUtil.hexString(pinBlock).toCharArray(); int offset = 2; int checkDigit = 0x2; int padidx = pinLength + offset; // test pin block validatePinBlock(block1,checkDigit,padidx,offset); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT35: { // Block 2 byte[] bl2 = ISOUtil.hex2byte("0000" + accountNumber); // get Block1 byte[] bl1 = ISOUtil.xor(pinBlock, bl2); int pinLength = bl1[0] & 0x0f; char[] block1 = ISOUtil.hexString(bl1).toCharArray(); int offset = 2; int checkDigit = 0x2; int padidx = pinLength + offset; // test pin block validatePinBlock(block1,checkDigit,padidx,offset); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT41: { // Block 2 - account number should contain Unique DEA Key A (UDK-A) byte[] bl2 = ISOUtil.hex2byte("00000000" + accountNumber.substring(accountNumber.length()-8) ); // get Block1 byte[] bl1 = ISOUtil.xor(pinBlock, bl2); int pinLength = bl1[0] & 0x0f; char[] block1 = ISOUtil.hexString(bl1).toCharArray(); int offset = 2; int checkDigit = 0x0; int padidx = pinLength + offset; // test pin block validatePinBlock(block1,checkDigit,padidx,offset); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; case FORMAT42: { // Block 2 - account number should contain Unique DEA Key A (UDK-A) byte[] bl2 = ISOUtil.hex2byte("00000000" + accountNumber.substring(accountNumber.length()-8) ); // get Block1 byte[] bl1 = ISOUtil.xor(pinBlock, bl2); int pinLength = bl1[0] & 0x0f; char[] block1 = ISOUtil.hexString(bl1).toCharArray(); int offset = 2; int checkDigit = 0x0; int padidx = pinLength + offset; // test pin block validatePinBlock(block1,checkDigit,padidx,offset,'0'); // get pin pin = new String(Arrays.copyOfRange(block1, offset, padidx)); } break; default: throw new SMException("Unsupported PIN Block format: " + pinBlockFormat); } return pin; } /** * Initializes the JCE Security Module * @param jceProviderClassName * @param lmkFile Local Master Keys File used by JCE Security Module to store the LMKs * @param lmkRebuild if set to true, the lmkFile gets overwritten with newly generated keys (WARNING: this would render all your previously stored SecureKeys unusable) * @throws SMException */ private void init (String jceProviderClassName, String lmkFile, boolean lmkRebuild) throws SMException { File lmk = lmkFile != null ? new File(lmkFile) : null; if (lmk == null && !lmkRebuild) throw new SMException ("null lmkFile - needs rebuild"); try { keyTypeToLMKIndex = new TreeMap<>(); keyTypeToLMKIndex.put(SMAdapter.TYPE_ZMK, 0x000); keyTypeToLMKIndex.put(SMAdapter.TYPE_ZPK, 0x001); keyTypeToLMKIndex.put(SMAdapter.TYPE_PVK, 0x002); keyTypeToLMKIndex.put(SMAdapter.TYPE_TPK, 0x002); keyTypeToLMKIndex.put(SMAdapter.TYPE_TMK, 0x002); keyTypeToLMKIndex.put(SMAdapter.TYPE_TAK, 0x003); // keyTypeToLMKIndex.put(PINLMKIndex, 0x004); keyTypeToLMKIndex.put(SMAdapter.TYPE_CVK, 0x402); keyTypeToLMKIndex.put(SMAdapter.TYPE_ZAK, 0x008); keyTypeToLMKIndex.put(SMAdapter.TYPE_BDK, 0x009); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_AC, 0x109); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_SMI, 0x209); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_SMC, 0x309); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_DAC, 0x409); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_DN, 0x509); keyTypeToLMKIndex.put(SMAdapter.TYPE_MK_CVC3, 0x709); keyTypeToLMKIndex.put(SMAdapter.TYPE_ZEK, 0x00A); keyTypeToLMKIndex.put(SMAdapter.TYPE_DEK, 0x00B); keyTypeToLMKIndex.put(SMAdapter.TYPE_RSA_SK, 0x00C); keyTypeToLMKIndex.put(SMAdapter.TYPE_HMAC, 0x10C); keyTypeToLMKIndex.put(SMAdapter.TYPE_RSA_PK, 0x00D); Provider provider; LogEvent evt = new LogEvent(this, "jce-provider"); try { if (jceProviderClassName == null || jceProviderClassName.isEmpty()) { evt.addMessage("No JCE Provider specified. Attempting to load default provider (SunJCE)."); jceProviderClassName = "com.sun.crypto.provider.SunJCE"; } provider = (Provider)Class.forName(jceProviderClassName).newInstance(); Security.addProvider(provider); evt.addMessage("name", provider.getName()); } catch (Exception e) { evt.addMessage(e); throw new SMException("Unable to load jce provider whose class name is: " + jceProviderClassName); } finally { Logger.log(evt); } jceHandler = new JCEHandler(provider); if (lmkRebuild) { // Creat new LMK file evt = new LogEvent(this, "local-master-keys"); if (lmk != null) evt.addMessage("Rebuilding new Local Master Keys in file: \"" + lmk.getCanonicalPath() + "\"."); Logger.log(evt); // Generate New random Local Master Keys generateLMK(); // Write the new Local Master Keys to file evt = new LogEvent(this, "local-master-keys"); if (lmk != null) { writeLMK(lmk); evt.addMessage("Local Master Keys built successfully in file: \"" + lmk.getCanonicalPath() + "\"."); } else { evt.addMessage("Local Master Keys built successfully"); } Logger.log(evt); } if (lmk != null) { if (!lmk.exists()) { // LMK File does not exist throw new SMException("Error loading Local Master Keys, file: \"" + lmk.getCanonicalPath() + "\" does not exist." + " Please specify a valid LMK file, or rebuild a new one."); } else { // Read LMK from file readLMK(lmk); evt = new LogEvent(this, "local-master-keys"); evt.addMessage("Loaded successfully from file: \"" + lmk.getCanonicalPath() + "\""); Logger.log(evt); } } } catch (Exception e) { if (e instanceof SMException) { throw (SMException)e; } else { throw new SMException(e); } } } private byte[] applySchemeVariant(byte[] lmkdata, int variant){ byte[] vardata = new byte[lmkdata.length]; System.arraycopy(lmkdata, 0, vardata, 0, lmkdata.length); //XOR first byte of second key with selected variant byte vardata[8] ^= variant; return vardata; } private byte[] applyVariant(byte[] lmkdata, int variant){ byte[] vardata = new byte[lmkdata.length]; System.arraycopy(lmkdata, 0, vardata, 0, lmkdata.length); //XOR first byte of first key with selected variant byte vardata[0] ^= variant; return vardata; } private void spreadLMKVariants(byte[] lmkData, int idx) throws SMException { int i = 0; for (int v :variants){ int k = 0; byte[] variantData = applyVariant(lmkData,v); for (int sv :schemeVariants){ byte[] svData = applySchemeVariant(variantData,sv); // System.out.println(String.format("LMK0x%1$d:%2$d:%3$02x=%4$s", k, i, idx // ,ISOUtil.hexString(svData))); // make it 3 components to work with sun JCE svData = ISOUtil.concat( svData, 0, jceHandler.getBytesLength(SMAdapter.LENGTH_DES3_2KEY), svData, 0, jceHandler.getBytesLength(SMAdapter.LENGTH_DES) ); int key = idx; key += 0x100*i; key += 0x1000*k++; lmks.put(key, (SecretKey)jceHandler.formDESKey(SMAdapter.LENGTH_DES3_2KEY, svData)); } i++; } } /** * Generates new LMK keys * @exception SMException */ private void generateLMK () throws SMException { lmks.clear(); try { for (int i = 0; i <= LMK_PAIRS_NO; i++){ SecretKey lmkKey = (SecretKey)jceHandler.generateDESKey(LMK_KEY_LENGTH); spreadLMKVariants(lmkKey.getEncoded(), i); } } catch (JCEHandlerException e) { throw new SMException("Can't generate Local Master Keys", e); } } /** * reads (loads) LMK's from lmkFile * @param lmkFile * @exception SMException */ private void readLMK (File lmkFile) throws SMException { lmks.clear(); try { Properties lmkProps = new Properties(); InputStream in = new BufferedInputStream(new FileInputStream(lmkFile)); try { lmkProps.load(in); } finally { in.close(); } Enumeration e = lmkProps.propertyNames(); while (e.hasMoreElements()) { String propName = (String) e.nextElement(); lmkProps.put(propName, Environment.get(lmkProps.getProperty(propName))); } byte[] lmkData; for (int i = 0; i <= LMK_PAIRS_NO; i++) { lmkData = ISOUtil.hex2byte(lmkProps.getProperty( String.format("LMK0x%1$02x", i)).substring(0, SMAdapter.LENGTH_DES3_2KEY/4)); spreadLMKVariants(lmkData, i); } } catch (Exception e) { throw new SMException("Can't read Local Master Keys from file: " + lmkFile, e); } } /** * Writes a newly generated LMK's to lmkFile * @param lmkFile * @exception SMException */ private void writeLMK (File lmkFile) throws SMException { Properties lmkProps = new Properties(); try { for (int i = 0; i <= LMK_PAIRS_NO; i++) { lmkProps.setProperty(String.format("LMK0x%1$02x", i), ISOUtil.hexString(lmks.get(i).getEncoded())); } OutputStream out = new BufferedOutputStream(new FileOutputStream(lmkFile)); try { lmkProps.store(out, "Local Master Keys"); } finally { out.close(); } } catch (Exception e) { throw new SMException("Can't write Local Master Keys to file: " + lmkFile, e); } } /** * gets the suitable LMK variant for the key index * @param lmkIndex * @return the lmks secret key for the givin key index * @throws SMException */ private SecretKey getLMK (Integer lmkIndex) throws SMException { SecretKey lmk = lmks.get(lmkIndex); if (lmk==null) throw new SMException(String.format("Invalid key code: LMK0x%1$04x", lmkIndex)); return lmk; } /** * maps a key type to an LMK Index */ private Map keyTypeToLMKIndex; /** * The clear Local Master Keys */ private final Map lmks = new TreeMap<>(); /** * A index for the LMK used to encrypt the PINs */ private static final Integer PINLMKIndex = 0x004; /** * The key length (in bits) of the Local Master Keys. * JCESecurityModule uses Triple DES Local Master Keys */ private static final short LMK_KEY_LENGTH = LENGTH_DES3_2KEY; /** * The minimum length of the PIN */ private static final short MIN_PIN_LENGTH = 4; /** * The maximum length of the PIN */ private static final short MAX_PIN_LENGTH = 12; /** * a 64-bit block of ones used when calculating pin blocks */ private static final byte[] fPaddingBlock = ISOUtil.hex2byte("FFFFFFFFFFFFFFFF"); /** * a dummy 64-bit block of zeros used when calculating the check value */ private static final byte[] zeroBlock = ISOUtil.hex2byte("0000000000000000"); protected JCEHandler jceHandler; //-------------------------------------------------------------------------------------------------- // DUKPT //-------------------------------------------------------------------------------------------------- private byte[] encrypt64(byte[] data, byte[] key) throws JCEHandlerException { return jceHandler.encryptData( data, jceHandler.formDESKey((short) (key.length << 3), key) ); } private byte[] decrypt64(byte[] data, byte[] key) throws JCEHandlerException { return jceHandler.decryptData( data, jceHandler.formDESKey((short) (key.length << 3), key) ); } protected byte[] specialEncrypt(byte[] data, byte[] key) throws JCEHandlerException { if (key.length == 8) { data = ISOUtil.xor(data, key); data = encrypt64(data, key); return ISOUtil.xor(data, key); } byte[] keyL = new byte[8]; byte[] keyR = new byte[8]; System.arraycopy(key, 0, keyL, 0, 8); System.arraycopy(key, 8, keyR, 0, 8); data = encrypt64(data, keyL); data = decrypt64(data, keyR); data = encrypt64(data, keyL); return data; } protected byte[] specialDecrypt(byte[] data, byte[] key) throws JCEHandlerException { if (key.length == 8) { data = ISOUtil.xor(data, key); data = decrypt64(data, key); return ISOUtil.xor(data, key); } byte[] keyL = new byte[8]; byte[] keyR = new byte[8]; System.arraycopy(key, 0, keyL, 0, 8); System.arraycopy(key, 8, keyR, 0, 8); data = decrypt64(data, keyL); data = encrypt64(data, keyR); data = decrypt64(data, keyL); return data; } private void shr(byte[] b) { boolean carry = false; for (int i = 0; i < b.length; i++) { byte c = b[i]; b[i] = (byte) (c >>> 1 & 0x7F); if (carry) { b[i] |= 0x80; } carry = (c & 0x01) == 1; } } private void or(byte[] b, byte[] mask, int offset) { int len = Math.min(b.length - offset, mask.length); byte[] d = new byte[len]; for (int i = 0; i < len; i++) { b[offset++] |= mask[i]; } } private byte[] and(byte[] b, byte[] mask, int offset) { int len = Math.min(b.length - offset, mask.length); byte[] d = new byte[b.length]; System.arraycopy(b, 0, d, 0, b.length); for (int i = 0; i < len; i++, offset++) { d[offset] = (byte) (b[offset] & mask[i]); } return d; } private byte[] and(byte[] b, byte[] mask) { return and(b, mask, 0); } private boolean notZero(byte[] b) { int l = b.length; for (int i = 0; i < l; i++) { if (b[i] != 0) { return true; } } return false; } private byte[] calculateInitialKey(KeySerialNumber sn, SecureDESKey bdk, boolean tdes) throws SMException { byte[] kl = new byte[8]; byte[] kr = new byte[8]; byte[] kk = decryptFromLMK(bdk).getEncoded(); System.arraycopy(kk, 0, kl, 0, 8); System.arraycopy(kk, 8, kr, 0, 8); String paddedKsn; try { paddedKsn = ISOUtil.padleft( sn.getBaseKeyID() + sn.getDeviceID() + sn.getTransactionCounter(), 20, 'F' ); } catch (ISOException e) { throw new SMException(e); } byte[] ksn = ISOUtil.hex2byte(paddedKsn.substring(0, 16)); ksn[7] &= 0xE0; byte[] data = encrypt64(ksn, kl); data = decrypt64(data, kr); data = encrypt64(data, kl); // ANS X9.24:2009 3DES keys (@DFLC/@apr 2011) A.6 steps 5 & 6 (p69) if (tdes) { byte[] kl2 = ISOUtil.xor(kl, _VARIANT_RIGHT_HALF); byte[] kr2 = ISOUtil.xor(kr, _VARIANT_RIGHT_HALF); byte[] data2 = encrypt64(ksn, kl2); data2 = decrypt64(data2, kr2); data2 = encrypt64(data2, kl2); byte[] d = new byte[16]; System.arraycopy(data, 0, d, 0, 8); System.arraycopy(data2, 0, d, 8, 8); data = d; } return data; } @Override public byte[] dataEncrypt (SecureDESKey bdk, byte[] clearText) throws SMException { try { byte[] ksnB = jceHandler.generateDESKey ((short) 128).getEncoded(); KeySerialNumber ksn = getKSN (ISOUtil.hexString(ksnB)); byte[] derivedKey = calculateDerivedKey (ksn, bdk, true, true); Key dk = jceHandler.formDESKey ((short) 128, derivedKey); byte[] cypherText = jceHandler.encryptData (lpack(clearText), dk); ByteBuffer bb = ByteBuffer.allocate (cypherText.length + 32); bb.put (ksnB); bb.put (cypherText); bb.put (hash8 (new byte[][]{ ksnB, cypherText, derivedKey })); return bb.array(); } catch (JCEHandlerException e) { throw new SMException (e); } } @Override public byte[] dataDecrypt (SecureDESKey bdk, byte[] cypherText) throws SMException { try { if (cypherText.length < 32) { throw new SMException ( "Invalid key block '" + ISOUtil.hexString (cypherText) + "'" ); } byte[] ksnB = new byte[24]; byte[] encryptedData = new byte[cypherText.length - 32]; byte[] mac = new byte[8]; System.arraycopy (cypherText, 0, ksnB, 0, 24); System.arraycopy (cypherText, 24, encryptedData, 0, encryptedData.length); System.arraycopy (cypherText, cypherText.length-8, mac, 0, 8); KeySerialNumber ksn = getKSN (ISOUtil.hexString(ksnB)); byte[] derivedKey = calculateDerivedKey (ksn, bdk, true, true); Key dk = jceHandler.formDESKey ((short) 128, derivedKey); byte[] clearText = jceHandler.decryptData (encryptedData, dk); byte[] generatedMac = hash8 ( new byte[][] { ksnB, encryptedData, derivedKey } ); if (!Arrays.equals (mac, generatedMac)) throw new SMException ("Invalid cyphertext."); return lunpack (clearText); } catch (JCEHandlerException e) { throw new SMException (e); } } protected byte[] calculateDerivedKey(KeySerialNumber ksn, SecureDESKey bdk, boolean tdes, boolean dataEncryption) throws SMException { return tdes?calculateDerivedKeyTDES(ksn,bdk, dataEncryption):calculateDerivedKeySDES(ksn,bdk); } private byte[] calculateDerivedKeySDES(KeySerialNumber ksn, SecureDESKey bdk) throws SMException { final byte[] _1FFFFF = new byte[]{(byte) 0x1F, (byte) 0xFF, (byte) 0xFF}; final byte[] _100000 = new byte[]{(byte) 0x10, (byte) 0x00, (byte) 0x00}; final byte[] _E00000 = new byte[]{(byte) 0xE0, (byte) 0x00, (byte) 0x00}; byte[] curkey = calculateInitialKey(ksn, bdk, false); byte[] smidr = ISOUtil.hex2byte( ksn.getBaseKeyID() + ksn.getDeviceID() + ksn.getTransactionCounter() ); byte[] reg3 = ISOUtil.hex2byte(ksn.getTransactionCounter()); reg3 = and(reg3, _1FFFFF); byte[] shiftr = _100000; byte[] temp; byte[] tksnr; smidr = and(smidr, _E00000, 5); do { temp = and(shiftr, reg3); if (notZero(temp)) { or(smidr, shiftr, 5); tksnr = ISOUtil.xor(smidr, curkey); tksnr = encrypt64(tksnr, curkey); curkey = ISOUtil.xor(tksnr, curkey); } shr(shiftr); } while (notZero(shiftr)); curkey[7] ^= 0xFF; return curkey; } private byte[] calculateDerivedKeyTDES(KeySerialNumber ksn, SecureDESKey bdk, boolean dataEncryption) throws SMException { final byte[] _1FFFFF = new byte[]{(byte) 0x1F, (byte) 0xFF, (byte) 0xFF}; final byte[] _100000 = new byte[]{(byte) 0x10, (byte) 0x00, (byte) 0x00}; final byte[] _E00000 = new byte[]{(byte) 0xE0, (byte) 0x00, (byte) 0x00}; byte[] curkey = calculateInitialKey(ksn, bdk, true); byte[] smidr = ISOUtil.hex2byte( ksn.getBaseKeyID() + ksn.getDeviceID() + ksn.getTransactionCounter() ); byte[] reg3 = ISOUtil.hex2byte(ksn.getTransactionCounter()); reg3 = and(reg3, _1FFFFF); byte[] shiftr = _100000; byte[] temp; byte[] tksnr; byte[] r8a; byte[] r8b; byte[] curkeyL = new byte[8]; byte[] curkeyR = new byte[8]; smidr = and(smidr, _E00000, 5); do { temp = and(shiftr, reg3); if (notZero(temp)) { System.arraycopy(curkey, 0, curkeyL, 0, 8); System.arraycopy(curkey, 8, curkeyR, 0, 8); or(smidr, shiftr, 5); // smidr == R8 tksnr = ISOUtil.xor(smidr, curkeyR); tksnr = encrypt64(tksnr, curkeyL); tksnr = ISOUtil.xor(tksnr, curkeyR); // tksnr == R8A curkeyL = ISOUtil.xor(curkeyL, _VARIANT_RIGHT_HALF); curkeyR = ISOUtil.xor(curkeyR, _VARIANT_RIGHT_HALF); r8b = ISOUtil.xor(smidr, curkeyR); r8b = encrypt64(r8b, curkeyL); r8b = ISOUtil.xor(r8b, curkeyR); System.arraycopy(r8b, 0, curkey, 0, 8); System.arraycopy(tksnr, 0, curkey, 8, 8); } shr(shiftr); } while (notZero(shiftr)); if (dataEncryption) { curkey[5] ^= 0xFF; curkey[13] ^= 0xFF; System.arraycopy(curkey, 0, curkeyL, 0, 8); System.arraycopy(curkey, 8, curkeyR, 0, 8); byte[] L = encrypt64(curkeyL, curkeyL); L = decrypt64(L, curkeyR); L = encrypt64(L, curkeyL); byte[] R = encrypt64(curkeyR, curkeyL); // this is the right implementation R = decrypt64(R, curkeyR); R = encrypt64(R, curkeyL); System.arraycopy (L, 0, curkey, 0, 8); System.arraycopy (R, 0, curkey, 8, 8); } else { curkey[7] ^= 0xFF; curkey[15] ^= 0xFF; } return curkey; } public SecureDESKey importBDK(String clearComponent1HexString, String clearComponent2HexString, String clearComponent3HexString) throws SMException { return formKEYfromThreeClearComponents((short) 128, "BDK", clearComponent1HexString, clearComponent2HexString, clearComponent3HexString); } private KeySerialNumber getKSN(String s) { return new KeySerialNumber( s.substring(0, 6), s.substring(6, 10), s.substring(10, Math.min(s.length(), 20)) ); } protected EncryptedPIN translatePINImpl (EncryptedPIN pinUnderDuk, KeySerialNumber ksn, SecureDESKey bdk, SecureDESKey kd2, byte destinationPINBlockFormat,boolean tdes) throws SMException { byte[] derivedKey = calculateDerivedKey(ksn, bdk, tdes, false); byte[] clearPinblk = specialDecrypt( pinUnderDuk.getPINBlock(), derivedKey ); String pan = pinUnderDuk.getAccountNumber(); String pin = calculatePIN( clearPinblk, pinUnderDuk.getPINBlockFormat(), pan ); byte[] translatedPinblk = jceHandler.encryptData( calculatePINBlock(pin, destinationPINBlockFormat, pan), decryptFromLMK(kd2) ); return new EncryptedPIN(translatedPinblk, destinationPINBlockFormat, pan,false); } protected EncryptedPIN importPINImpl (EncryptedPIN pinUnderDuk, KeySerialNumber ksn, SecureDESKey bdk,boolean tdes) throws SMException { byte[] derivedKey = calculateDerivedKey(ksn, bdk,tdes, false); byte[] clearPinblk = specialDecrypt( pinUnderDuk.getPINBlock(), derivedKey ); String pan = pinUnderDuk.getAccountNumber(); String pin = calculatePIN( clearPinblk, pinUnderDuk.getPINBlockFormat(), pan ); byte[] pinUnderLmk = jceHandler.encryptData( calculatePINBlock(pin, SMAdapter.FORMAT00, pan), getLMK(PINLMKIndex) ); return new EncryptedPIN(pinUnderLmk, SMAdapter.FORMAT00, pan,false); } /** * Exports PIN to DUKPT Encryption. * * @param pinUnderLmk * @param ksn * @param bdk * @param tdes * @param destinationPINBlockFormat * @return The encrypted pin * @throws SMException */ public EncryptedPIN exportPIN (EncryptedPIN pinUnderLmk, KeySerialNumber ksn, SecureDESKey bdk, boolean tdes, byte destinationPINBlockFormat) throws SMException { String accountNumber = pinUnderLmk.getAccountNumber(); // process // get clear PIN byte[] clearPINBlock = jceHandler.decryptData(pinUnderLmk.getPINBlock(), getLMK(PINLMKIndex)); // extract clear pin String pin = calculatePIN(clearPINBlock, pinUnderLmk.getPINBlockFormat(), accountNumber); clearPINBlock = calculatePINBlock(pin, destinationPINBlockFormat, accountNumber); // encrypt PIN byte[] derivedKey = calculateDerivedKey(ksn, bdk, tdes, false); byte[] translatedPINBlock = specialEncrypt(clearPINBlock, derivedKey); return new EncryptedPIN(translatedPINBlock, destinationPINBlockFormat, accountNumber, false); } /** * places a length indicator in a byte array. * @param b the byte array * @return a byte array with a two-byte length indicator */ private byte[] lpack (byte[] b) { int l = b.length; int adjustedLen = ((l+9) >> 3) << 3; byte[] d = new byte[adjustedLen]; System.arraycopy (b, 0, d, 2, l); d[0] = (byte) ((l >> 8) & 0xFF); d[1] = (byte) (l & 0xFF); return d; } /** * Unpacks a byte array packed by lPack * into the former byte[] * @param b packed byte array * @return original (unpacked) byte array */ private byte[] lunpack (byte[] b) { int l = ((((int)b[0])&0xFF) << 8) | (((int)b[1])&0xFF); byte[] d = new byte[l]; System.arraycopy (b,2,d,0,l); return d; } private byte[] hash8 (byte[][] bb) throws SMException { try { MessageDigest md = MessageDigest.getInstance("SHA"); for (byte[] b : bb) { md.update(b); } return Arrays.copyOf(md.digest(), 8); } catch (NoSuchAlgorithmException e) { throw new SMException (e); } } }





  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy