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

org.eclipse.keyple.card.calypso.CommandOpenSecureSession Maven / Gradle / Ivy

There is a newer version: 3.1.5
Show newest version
/* **************************************************************************************
 * Copyright (c) 2018 Calypso Networks Association https://calypsonet.org/
 *
 * See the NOTICE file(s) distributed with this work for additional information
 * regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the terms of the
 * Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 ************************************************************************************** */
package org.eclipse.keyple.card.calypso;

import static org.eclipse.keyple.card.calypso.DtoAdapters.*;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.keyple.core.util.ApduUtil;
import org.eclipse.keyple.core.util.ByteArrayUtil;
import org.eclipse.keyple.core.util.HexUtil;
import org.eclipse.keypop.calypso.card.WriteAccessLevel;
import org.eclipse.keypop.calypso.card.transaction.CryptoException;
import org.eclipse.keypop.calypso.card.transaction.CryptoIOException;
import org.eclipse.keypop.calypso.card.transaction.UnauthorizedKeyException;
import org.eclipse.keypop.calypso.crypto.asymmetric.AsymmetricCryptoException;
import org.eclipse.keypop.calypso.crypto.asymmetric.transaction.spi.AsymmetricCryptoCardTransactionManagerSpi;
import org.eclipse.keypop.calypso.crypto.symmetric.SymmetricCryptoException;
import org.eclipse.keypop.calypso.crypto.symmetric.SymmetricCryptoIOException;
import org.eclipse.keypop.card.ApduResponseApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Builds the Open Secure Session APDU command.
 *
 * @since 2.0.1
 */
final class CommandOpenSecureSession extends Command {

  private static final Logger logger = LoggerFactory.getLogger(CommandOpenSecureSession.class);
  private static final String PATTERN_1_BYTE_HEX = "%02Xh";

  private static final Map STATUS_TABLE;

  static {
    Map m = new HashMap<>(Command.STATUS_TABLE);
    m.put(
        0x6700,
        new StatusProperties("Lc value not supported", CardIllegalParameterException.class));
    m.put(0x6900, new StatusProperties("Transaction Counter is 0", CardTerminatedException.class));
    m.put(
        0x6981,
        new StatusProperties(
            "Command forbidden (read requested and current EF is a Binary file)",
            CardDataAccessException.class));
    m.put(
        0x6982,
        new StatusProperties(
            "Security conditions not fulfilled (PIN code not presented, AES key forbidding the "
                + "compatibility mode, encryption required)",
            CardSecurityContextException.class));
    m.put(
        0x6985,
        new StatusProperties(
            "Access forbidden (Never access mode, Session already opened)",
            CardAccessForbiddenException.class));
    m.put(
        0x6986,
        new StatusProperties(
            "Command not allowed (read requested and no current EF)",
            CardDataAccessException.class));
    m.put(0x6A81, new StatusProperties("Wrong key index", CardIllegalParameterException.class));
    m.put(0x6A82, new StatusProperties("File not found", CardDataAccessException.class));
    m.put(
        0x6A83,
        new StatusProperties(
            "Record not found (record index is above NumRec)", CardDataAccessException.class));
    m.put(
        0x6B00,
        new StatusProperties(
            "P1 or P2 value not supported (key index incorrect, wrong P2, extended mode not supported)",
            CardIllegalParameterException.class));
    m.put(0x61FF, new StatusProperties("Correct execution (ISO7816 T=0)"));
    m.put(
        0x6200,
        new StatusProperties(
            "Successful execution, with warning (Pre-Open variant, secure session not opened)"));
    STATUS_TABLE = m;
  }

  private final WriteAccessLevel writeAccessLevel;
  private final boolean isExtendedModeAllowed;
  private SymmetricCryptoSecuritySettingAdapter symmetricCryptoSecuritySetting;
  private transient boolean isPreOpenModeOnSelection; // NOSONAR
  private transient boolean isPreOpenMode; // NOSONAR
  private transient byte[] preOpenDataOut; // NOSONAR
  private transient boolean isReadModeConfigured; // NOSONAR
  private int sfi;
  private int recordNumber;

  private transient boolean isPreviousSessionRatified; // NOSONAR
  private byte[] challengeTransactionCounter;
  private Byte kif;
  private Byte kvc;
  private byte[] recordData;

  /**
   * Constructor for "pre-open" variant (to be used for card selection only).
   *
   * @param transactionContext The global transaction context common to all commands.
   * @param commandContext The local command context specific to each command.
   * @param writeAccessLevel The write access level.
   * @since 2.3.3
   */
  CommandOpenSecureSession(
      TransactionContextDto transactionContext,
      CommandContextDto commandContext,
      WriteAccessLevel writeAccessLevel) {
    super(CardCommandRef.OPEN_SECURE_SESSION, 0, transactionContext, commandContext);
    isPreOpenModeOnSelection = true;
    isExtendedModeAllowed = true;
    this.writeAccessLevel = writeAccessLevel;
    createRev3((byte) (writeAccessLevel.ordinal() + 1), new byte[0]); // with no SAM challenge
    if (logger.isDebugEnabled()) {
      addSubName("pre-open");
    }
  }

  /**
   * Partial constructor.
   *
   * @param transactionContext The global transaction context common to all commands.
   * @param commandContext The local command context specific to each command.
   * @param symmetricCryptoSecuritySetting The symmetric crypto security settings to use.
   * @param writeAccessLevel The write access level.
   * @param isExtendedModeAllowed Is the extended mode allowed?
   * @since 2.3.2
   */
  CommandOpenSecureSession(
      TransactionContextDto transactionContext,
      CommandContextDto commandContext,
      SymmetricCryptoSecuritySettingAdapter symmetricCryptoSecuritySetting,
      WriteAccessLevel writeAccessLevel,
      boolean isExtendedModeAllowed) {
    super(CardCommandRef.OPEN_SECURE_SESSION, 0, transactionContext, commandContext);
    this.writeAccessLevel = writeAccessLevel;
    this.symmetricCryptoSecuritySetting = symmetricCryptoSecuritySetting;
    this.isExtendedModeAllowed = isExtendedModeAllowed;
    preOpenDataOut = transactionContext.getCard().getPreOpenDataOut();
    isPreOpenMode = preOpenDataOut != null;
  }

  /**
   * Constructor for PKI mode variant.
   *
   * @param transactionContext The global transaction context common to all commands.
   * @param commandContext The local command context specific to each command.
   * @param terminalChallenge The terminal challenge.
   * @since 3.1.0
   */
  CommandOpenSecureSession(
      TransactionContextDto transactionContext,
      CommandContextDto commandContext,
      byte[] terminalChallenge) {
    super(CardCommandRef.OPEN_SECURE_SESSION, 0, transactionContext, commandContext);
    isPreOpenModeOnSelection = false;
    isExtendedModeAllowed = true;
    this.writeAccessLevel = null;
    createRev3Pki(terminalChallenge);
    if (logger.isDebugEnabled()) {
      addSubName("PKI");
    }
  }

  /**
   * Create Rev 3
   *
   * @param keyIndex the key index.
   * @param samChallenge the sam challenge returned by the SAM Get Challenge APDU command.
   * @throws IllegalArgumentException If the request is inconsistent
   */
  private void createRev3(byte keyIndex, byte[] samChallenge) {

    byte p1 = (byte) ((recordNumber * 8) + keyIndex);
    byte p2;
    byte[] dataIn;

    if (isExtendedModeAllowed) {
      p2 = (byte) ((sfi * 8) + 2);
      dataIn = new byte[samChallenge.length + 1];
      System.arraycopy(samChallenge, 0, dataIn, 1, samChallenge.length);
    } else {
      p2 = (byte) ((sfi * 8) + 1);
      dataIn = samChallenge;
    }

    /*
     * case 4: this command contains incoming and outgoing data. We define le = 0, the actual
     * length will be processed by the lower layers.
     */
    setApduRequest(
        new ApduRequestAdapter(
            ApduUtil.build(
                CalypsoCardClass.ISO.getValue(),
                CardCommandRef.OPEN_SECURE_SESSION.getInstructionByte(),
                p1,
                p2,
                dataIn,
                (byte) 0)));

    if (logger.isDebugEnabled()) {
      addSubName(
          "key index: " + keyIndex + ", sfi: " + HexUtil.toHex(sfi) + "h, rec: " + recordNumber);
    }
  }

  /**
   * Create Rev 2.4
   *
   * @param keyIndex the key index.
   * @param samChallenge the sam challenge returned by the SAM Get Challenge APDU command.
   */
  private void createRev24(byte keyIndex, byte[] samChallenge) {
    byte p1 = (byte) (0x80 + (recordNumber * 8) + keyIndex);
    buildLegacyApduRequest(keyIndex, samChallenge, sfi, recordNumber, p1);
  }

  /**
   * Create Rev 1.0
   *
   * @param keyIndex the key index.
   * @param samChallenge the sam challenge returned by the SAM Get Challenge APDU command.
   */
  private void createRev10(byte keyIndex, byte[] samChallenge) {
    byte p1 = (byte) ((recordNumber * 8) + keyIndex);
    buildLegacyApduRequest(keyIndex, samChallenge, sfi, recordNumber, p1);
  }

  /**
   * Create Rev 3 for PKI mode
   *
   * @param terminalChallenge the terminal challenge.
   * @throws IllegalArgumentException If the request is inconsistent
   */
  private void createRev3Pki(byte[] terminalChallenge) {

    final byte p1 = 0x00;
    final byte p2 = 0x03;
    byte[] dataIn = new byte[terminalChallenge.length + 1];
    System.arraycopy(terminalChallenge, 0, dataIn, 1, terminalChallenge.length);

    /*
     * case 4: this command contains incoming and outgoing data. We define le = 0, the actual
     * length will be processed by the lower layers.
     */
    setApduRequest(
        new ApduRequestAdapter(
            ApduUtil.build(
                CalypsoCardClass.ISO.getValue(),
                CardCommandRef.OPEN_SECURE_SESSION.getInstructionByte(),
                p1,
                p2,
                dataIn,
                (byte) 0)));

    if (logger.isDebugEnabled()) {
      addSubName("sfi: " + HexUtil.toHex(sfi) + "h, rec: " + recordNumber);
    }
  }

  /**
   * Build legacy apdu request.
   *
   * @param keyIndex the key index.
   * @param samChallenge the sam challenge returned by the SAM Get Challenge APDU command.
   * @param sfi the sfi to select.
   * @param recordNumber the record number to read.
   * @param p1 P1.
   * @throws IllegalArgumentException If the request is inconsistent
   */
  private void buildLegacyApduRequest(
      byte keyIndex, byte[] samChallenge, int sfi, int recordNumber, byte p1) {

    byte p2 = (byte) (sfi * 8);
    /*
     * case 4: this command contains incoming and outgoing data. We define le = 0, the actual
     * length will be processed by the lower layers.
     */
    setApduRequest(
        new ApduRequestAdapter(
            ApduUtil.build(
                CalypsoCardClass.LEGACY.getValue(),
                CardCommandRef.OPEN_SECURE_SESSION.getInstructionByte(),
                p1,
                p2,
                samChallenge,
                (byte) 0)));

    if (logger.isDebugEnabled()) {
      addSubName(
          "key index: " + keyIndex + ", sfi: " + HexUtil.toHex(sfi) + "h, rec: " + recordNumber);
    }
  }

  /**
   * Configures the read mode.
   *
   * @param sfi The SFI to select.
   * @param recordNumber The number of the record to read.
   * @since 2.3.2
   */
  void configureReadMode(int sfi, int recordNumber) {
    if (getTransactionContext().isPkiMode()) {
      byte[] apdu = getApduRequest().getApdu();
      // overwrite p1 & p2
      apdu[2] = (byte) (recordNumber * 8);
      apdu[3] = (byte) ((sfi * 8) + 3);
      if (logger.isDebugEnabled()) {
        addSubName("sfi: " + HexUtil.toHex(sfi) + "h, rec: " + recordNumber);
      }
    } else {
      this.sfi = sfi;
      this.recordNumber = recordNumber;
    }
    isReadModeConfigured = true;
  }

  /**
   * @return "true" if the read mode is already configured.
   * @since 2.3.2
   */
  boolean isReadModeConfigured() {
    return isReadModeConfigured;
  }

  /**
   * {@inheritDoc}
   *
   * @since 2.3.2
   */
  @Override
  void finalizeRequest() {
    byte[] samChallenge;
    try {
      samChallenge =
          getTransactionContext()
              .getSymmetricCryptoCardTransactionManagerSpi()
              .initTerminalSecureSessionContext();
    } catch (SymmetricCryptoException e) {
      throw new CryptoException(e.getMessage(), e);
    } catch (SymmetricCryptoIOException e) {
      throw new CryptoIOException(e.getMessage(), e);
    }
    byte keyIndex = (byte) (writeAccessLevel.ordinal() + 1);
    switch (getTransactionContext().getCard().getProductType()) {
      case PRIME_REVISION_1:
        createRev10(keyIndex, samChallenge);
        break;
      case PRIME_REVISION_2:
        createRev24(keyIndex, samChallenge);
        break;
      case PRIME_REVISION_3:
      case LIGHT:
      case BASIC:
        createRev3(keyIndex, samChallenge);
        break;
      default:
        throw new IllegalArgumentException(
            "Product type "
                + getTransactionContext().getCard().getProductType()
                + " not supported");
    }
  }

  /**
   * {@inheritDoc}
   *
   * @since 2.3.2
   */
  @Override
  boolean isCryptoServiceRequiredToFinalizeRequest() {
    return true;
  }

  /**
   * {@inheritDoc}
   *
   * @since 2.3.2
   */
  @Override
  boolean synchronizeCryptoServiceBeforeCardProcessing() {
    if (!isPreOpenMode) {
      return false;
    }
    // In pre-open mode, we can synchronize the crypto service without having to execute the card
    // open session command first.
    if (!isCryptoServiceSynchronized()) {
      parseRev3(preOpenDataOut);
      synchronizeCryptoService(preOpenDataOut);
    }
    return true;
  }

  /**
   * Synchronizes the crypto service.
   *
   * @param dataOut The APDU dataOut field.
   */
  private void synchronizeCryptoService(byte[] dataOut) {
    if (getTransactionContext().isPkiMode()) {
      try {
        AsymmetricCryptoCardTransactionManagerSpi cryptoManager =
            getTransactionContext().getAsymmetricCryptoCardTransactionManagerSpi();
        cryptoManager.initTerminalPkiSession(
            getTransactionContext().getCard().getCardPublicKeySpi());
        cryptoManager.updateTerminalPkiSession(getApduRequest().getApdu());
        cryptoManager.updateTerminalPkiSession(getApduResponse().getApdu());
      } catch (AsymmetricCryptoException e) {
        throw new CryptoException(e.getMessage(), e);
      }
    } else {
      Byte computedKvc = computeKvc();
      Byte computedKif = computeKif(computedKvc);
      if (!symmetricCryptoSecuritySetting.isSessionKeyAuthorized(computedKif, computedKvc)) {
        throw new UnauthorizedKeyException(
            String.format(
                "Unauthorized key error: KIF=%s, KVC=%s",
                computedKif != null ? String.format(PATTERN_1_BYTE_HEX, computedKif) : null,
                computedKvc != null ? String.format(PATTERN_1_BYTE_HEX, computedKvc) : null));
      }
      try {
        getTransactionContext()
            .getSymmetricCryptoCardTransactionManagerSpi()
            .initTerminalSessionMac(dataOut, computedKif, computedKvc);
      } catch (SymmetricCryptoException e) {
        throw new CryptoException(e.getMessage(), e);
      } catch (SymmetricCryptoIOException e) {
        throw new CryptoIOException(e.getMessage(), e);
      }
    }
    confirmCryptoServiceSuccessfullySynchronized();
  }

  /**
   * {@inheritDoc}
   *
   * @since 2.3.2
   */
  @Override
  void parseResponse(ApduResponseApi apduResponse) throws CardCommandException {
    super.setApduResponseAndCheckStatus(apduResponse);
    CalypsoCardAdapter card = getTransactionContext().getCard();
    if (!isPreOpenModeOnSelection) {
      card.backupFiles();
      getTransactionContext().setSecureSessionOpen(true);
    }
    // Parse data
    byte[] dataOut = getApduResponse().getDataOut();
    if (getTransactionContext().isPkiMode()) {
      parsePki(dataOut);
    } else {
      switch (card.getProductType()) {
        case PRIME_REVISION_1:
          parseRev10(dataOut);
          break;
        case PRIME_REVISION_2:
          parseRev24(dataOut);
          break;
        default:
          parseRev3(dataOut);
      }
    }
    // Update Calypso card image
    // CL-CSS-INFORAT.1
    card.setDfRatified(isPreviousSessionRatified);
    // CL-CSS-INFOTCNT.1
    card.setTransactionCounter(ByteArrayUtil.extractInt(challengeTransactionCounter, 0, 3, false));
    if (recordData.length > 0) {
      card.setContent((byte) sfi, recordNumber, recordData);
    }
    // If it is a pre-open variant, then we save the pre-open data into the Calypso card image.
    if (isPreOpenModeOnSelection && apduResponse.getStatusWord() == 0x6200) {
      card.setPreOpenWriteAccessLevel(writeAccessLevel);
      card.setPreOpenDataOut(dataOut);
    }
    // Synchronize crypto service
    if (!isCryptoServiceSynchronized()) {
      synchronizeCryptoService(dataOut);
    } else {
      // If the crypto service is already synchronized, this means you're in pre-open mode.
      if (!Arrays.equals(dataOut, preOpenDataOut)) {
        throw new CardSecurityContextException(
            "Session has been pre-opened but 'dataOut' fields do not match",
            CardCommandRef.OPEN_SECURE_SESSION);
      }
    }
  }

  /**
   * Returns the KVC to use according to the provided write access and the card's KVC.
   *
   * @return "null" if the card did not provide a KVC value and if there's no default KVC value.
   */
  private Byte computeKvc() {
    if (kvc != null) {
      return kvc;
    }
    return symmetricCryptoSecuritySetting.getDefaultKvc(writeAccessLevel);
  }

  /**
   * Returns the KIF to use according to the provided write access level and KVC.
   *
   * @param kvc The previously computed KVC value.
   * @return "null" if the card did not provide a KIF value and if there's no default KIF value.
   */
  private Byte computeKif(Byte kvc) {
    // CL-KEY-KIF.1
    if ((kif != null && kif != (byte) 0xFF) || (kvc == null)) {
      return kif;
    }
    // CL-KEY-KIFUNK.1
    Byte result = symmetricCryptoSecuritySetting.getKif(writeAccessLevel, kvc);
    if (result == null) {
      result = symmetricCryptoSecuritySetting.getDefaultKif(writeAccessLevel);
    }
    return result;
  }

  /**
   * Parse Rev 3
   *
   * @param apduResponseData The response data.
   */
  private void parseRev3(byte[] apduResponseData) {
    int offset;
    // CL-CSS-OSSRFU.1
    if (isExtendedModeAllowed) {
      offset = 4;
      isPreviousSessionRatified = (apduResponseData[8] & 0x01) == (byte) 0x00;
      boolean manageSecureSessionAuthorized = (apduResponseData[8] & 0x02) == (byte) 0x02;
      if (!manageSecureSessionAuthorized) {
        getTransactionContext().getCard().disableExtendedMode();
      }
    } else {
      offset = 0;
      isPreviousSessionRatified = (apduResponseData[4] == (byte) 0x00);
      getTransactionContext().getCard().disableExtendedMode();
    }
    challengeTransactionCounter = Arrays.copyOfRange(apduResponseData, 0, 3);
    kif = apduResponseData[5 + offset];
    kvc = apduResponseData[6 + offset];
    int dataLength = apduResponseData[7 + offset];
    recordData = Arrays.copyOfRange(apduResponseData, 8 + offset, 8 + offset + dataLength);
  }

  /**
   * Parse Rev 2.4
   *
   * 

In rev 2.4 mode, the response to the Open Secure Session command is as follows: * *

KK CC CC CC CC [RR RR] [NN..NN] * *

Where: * *

    *
  • KK = KVC byte CC *
  • CC CC CC CC = card challenge *
  • RR RR = ratification bytes (may be absent) *
  • NN..NN = record data (29 bytes) *
* * Legal length values are: * *
    *
  • 5: ratified, 1-byte KCV, 4-byte challenge, no data *
  • 34: ratified, 1-byte KCV, 4-byte challenge, 29 bytes of data *
  • 7: not ratified (2 ratification bytes), 1-byte KCV, 4-byte challenge, no data *
  • 35 not ratified (2 ratification bytes), 1-byte KCV, 4-byte challenge, 29 bytes of data *
* * @param apduResponseData The response data. */ private void parseRev24(byte[] apduResponseData) { switch (apduResponseData.length) { case 5: isPreviousSessionRatified = true; recordData = new byte[0]; break; case 34: isPreviousSessionRatified = true; recordData = Arrays.copyOfRange(apduResponseData, 5, 34); break; case 7: isPreviousSessionRatified = false; recordData = new byte[0]; break; case 36: isPreviousSessionRatified = false; recordData = Arrays.copyOfRange(apduResponseData, 7, 36); break; default: throw new IllegalStateException( "Bad response length to Open Secure Session: " + apduResponseData.length); } challengeTransactionCounter = Arrays.copyOfRange(apduResponseData, 1, 4); kif = null; kvc = apduResponseData[0]; } /** * Parse Rev 1.0 * *

In rev 1.0 mode, the response to the Open Secure Session command is as follows: * *

CC CC CC CC [RR RR] [NN..NN] * *

Where: * *

    *
  • CC CC CC CC = card challenge *
  • RR RR = ratification bytes (may be absent) *
  • NN..NN = record data (29 bytes) *
* * Legal length values are: * *
    *
  • 4: ratified, 4-byte challenge, no data *
  • 33: ratified, 4-byte challenge, 29 bytes of data *
  • 6: not ratified (2 ratification bytes), 4-byte challenge, no data *
  • 35 not ratified (2 ratification bytes), 4-byte challenge, 29 bytes of data *
* * @param apduResponseData The response data. */ private void parseRev10(byte[] apduResponseData) { switch (apduResponseData.length) { case 4: isPreviousSessionRatified = true; recordData = new byte[0]; break; case 33: isPreviousSessionRatified = true; recordData = Arrays.copyOfRange(apduResponseData, 4, 33); break; case 6: isPreviousSessionRatified = false; recordData = new byte[0]; break; case 35: isPreviousSessionRatified = false; recordData = Arrays.copyOfRange(apduResponseData, 6, 35); break; default: throw new IllegalStateException( "Bad response length to Open Secure Session: " + apduResponseData.length); } challengeTransactionCounter = Arrays.copyOfRange(apduResponseData, 0, 3); kif = null; kvc = null; } /** * Parse the command response in PKI mode. * *

Extracts the field pkiAid, pkiSerialNumber and pkiStatus. * * @param apduResponseData The card output data. * @throws IllegalStateException If the response is inconsistent. */ private void parsePki(byte[] apduResponseData) { int li = apduResponseData[0] & 0xFF; int offset = 1 + li + 8 + 1; challengeTransactionCounter = Arrays.copyOfRange(apduResponseData, offset, offset + 3); offset += 8; isPreviousSessionRatified = (apduResponseData[offset] & 0x01) == (byte) 0x00; offset += 1 + 2; int ld = apduResponseData[offset] & 0xFF; offset += 1; recordData = Arrays.copyOfRange(apduResponseData, offset, offset + ld); } /** * {@inheritDoc} * * @since 2.0.1 */ @Override Map getStatusTable() { return STATUS_TABLE; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy