org.eclipse.keyple.card.calypso.CalypsoCardAdapter Maven / Gradle / Ivy
Show all versions of keyple-card-calypso-java-lib Show documentation
/* **************************************************************************************
* 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.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.keyple.core.util.ByteArrayUtil;
import org.eclipse.keyple.core.util.HexUtil;
import org.eclipse.keyple.core.util.json.JsonUtil;
import org.eclipse.keypop.calypso.card.WriteAccessLevel;
import org.eclipse.keypop.calypso.card.card.*;
import org.eclipse.keypop.calypso.crypto.asymmetric.certificate.spi.CardPublicKeySpi;
import org.eclipse.keypop.card.ApduResponseApi;
import org.eclipse.keypop.card.CardSelectionResponseApi;
import org.eclipse.keypop.card.spi.SmartCardSpi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link CalypsoCard}.
*
* @since 2.0.0
*/
final class CalypsoCardAdapter implements CalypsoCard, SmartCardSpi {
private static final Logger logger = LoggerFactory.getLogger(CalypsoCardAdapter.class);
private static final int CARD_REV1_ATR_LENGTH = 20;
private static final int REV1_CARD_DEFAULT_WRITE_OPERATIONS_NUMBER_SUPPORTED_PER_SESSION = 3;
private static final int REV2_CARD_DEFAULT_WRITE_OPERATIONS_NUMBER_SUPPORTED_PER_SESSION = 6;
private static final int SI_BUFFER_SIZE_INDICATOR = 0;
private static final int SI_PLATFORM = 1;
private static final int SI_APPLICATION_TYPE = 2;
private static final int SI_APPLICATION_SUBTYPE = 3;
private static final int SI_SOFTWARE_ISSUER = 4;
private static final int SI_SOFTWARE_VERSION = 5;
private static final int SI_SOFTWARE_REVISION = 6;
private static final int DEFAULT_PAYLOAD_CAPACITY = 250;
// Application type bitmasks features
private static final byte APP_TYPE_WITH_CALYPSO_PIN = 0x01;
private static final byte APP_TYPE_WITH_CALYPSO_SV = 0x02;
private static final byte APP_TYPE_RATIFICATION_COMMAND_REQUIRED = 0x04;
private static final byte APP_TYPE_CALYPSO_REV_32_MODE = 0x08;
private static final byte APP_TYPE_WITH_PUBLIC_AUTHENTICATION = 0x10;
// buffer indicator to buffer size lookup table
private static final int[] BUFFER_SIZE_INDICATOR_TO_BUFFER_SIZE =
new int[] {
0, 0, 0, 0, 0, 0, 215, 256, 304, 362, 430, 512, 608, 724, 861, 1024, 1217, 1448, 1722, 2048,
2435, 2896, 3444, 4096, 4870, 5792, 6888, 8192, 9741, 11585, 13777, 16384, 19483, 23170,
27554, 32768, 38967, 46340, 55108, 65536, 77935, 92681, 110217, 131072, 155871, 185363,
220435, 262144, 311743, 370727, 440871, 524288, 623487, 741455, 881743, 1048576
};
private ApduResponseApi selectApplicationResponse;
private String powerOnData;
private boolean isExtendedModeSupported;
private boolean isRatificationOnDeselectSupported;
private boolean isSvFeatureAvailable;
private boolean isPinFeatureAvailable;
private boolean isPkiModeSupported;
private boolean isDfInvalidated;
private CalypsoCardClass calypsoCardClass;
private byte[] calypsoSerialNumber;
private byte[] startupInfo;
private ProductType productType = ProductType.UNKNOWN;
private byte[] dfName;
private int modificationsCounterMax;
private boolean isModificationCounterInBytes = true;
private DirectoryHeader directoryHeader;
private final Set files = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set filesBackup =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private ElementaryFileAdapter currentEf;
private Boolean isDfRatified;
private Integer transactionCounter;
private Integer pinAttemptCounter;
private Integer svBalance;
private int svLastTNum;
private Integer svBalanceBackup;
private int svLastTNumBackup;
private boolean isHce;
private byte[] challenge;
private byte[] traceabilityInformation;
private CardPublicKeySpi cardPublicKeySpi;
private byte[] cardPublicKey;
private ByteBuffer cardCertificate;
private ByteBuffer caCertificate;
private byte svKvc;
private byte[] svGetHeader;
private byte[] svGetData;
private byte[] svOperationSignature;
private byte applicationSubType;
private byte applicationType;
private byte sessionModification;
private int payloadCapacity = DEFAULT_PAYLOAD_CAPACITY;
private boolean isCounterValuePostponed;
private boolean isLegacyCase1;
private WriteAccessLevel preOpenWriteAccessLevel;
private byte[] preOpenDataOut;
private static final List patchesRev3 = new ArrayList<>();
private static final List patchesRev12 = new ArrayList<>();
static {
// Patches for revision 3:
// XX 3C XX XX XX 10 XX
patchesRev3.add(new PatchRev3("003C0000001000", "00FF000000FF00").setPayloadCapacity(235));
// Patches for revision 1 & 2:
// 06 XX 01 03 XX XX XX
patchesRev12.add(new PatchRev12("06000103000000", "FF00FFFF000000").setCounterValuePostponed());
// 06 0A 01 02 XX XX XX
patchesRev12.add(new PatchRev12("060A0102000000", "FFFFFFFF000000").setCounterValuePostponed());
// XX XX 0X XX 15 XX XX
patchesRev12.add(new PatchRev12("00000000150000", "0000F000FF0000").setCounterValuePostponed());
// XX XX 1X XX 15 XX XX
patchesRev12.add(new PatchRev12("00001000150000", "0000F000FF0000").setCounterValuePostponed());
// 0A 0A 01 02 20 03 11: PACA Card
patchesRev12.add(new PatchRev12("0A0A0102200311", "FFFFFFFFFFFFFF").setCounterValuePostponed());
// 03 08 03 04 00 02 00: targets ASK Tango having this startup info values
patchesRev12.add(new PatchRev12("03080304000200", "FFFFFFFFFFFFFF").setLegacyCase1());
}
/**
* Constructor.
*
* @since 2.0.0
*/
CalypsoCardAdapter(CardSelectionResponseApi cardSelectionResponse) throws CardCommandException {
if (cardSelectionResponse != null) {
if (cardSelectionResponse.getSelectApplicationResponse() != null) {
initializeWithFci(cardSelectionResponse.getSelectApplicationResponse());
} else if (cardSelectionResponse.getPowerOnData() != null) {
initializeWithPowerOnData(cardSelectionResponse.getPowerOnData());
}
}
}
/**
* Initializes the object with the card power-on data.
*
* This method should be invoked only when no response to select application is available.
*
* @param powerOnData The card's power-on data.
* @throws IllegalArgumentException If powerOnData is inconsistent.
*/
private void initializeWithPowerOnData(String powerOnData) {
productType = ProductType.PRIME_REVISION_1;
calypsoCardClass = CalypsoCardClass.LEGACY;
this.powerOnData = powerOnData;
// FCI is not provided: we consider it is Calypso card rev 1, it's serial number is provided in
// the ATR
byte[] atr = HexUtil.toByteArray(powerOnData);
// basic check: we expect to be here following a selection based on the ATR
if (atr.length != CARD_REV1_ATR_LENGTH) {
throw new IllegalArgumentException("Unexpected ATR length: " + powerOnData);
}
dfName = null;
calypsoSerialNumber = new byte[8];
// old cards have their modification counter expressed in number of commands
// the array is initialized with 0 (cf. default value for primitive types)
System.arraycopy(atr, 12, calypsoSerialNumber, 4, 4);
modificationsCounterMax = REV1_CARD_DEFAULT_WRITE_OPERATIONS_NUMBER_SUPPORTED_PER_SESSION;
startupInfo = new byte[7];
// create buffer size indicator
startupInfo[0] = (byte) modificationsCounterMax;
// create the startup info with the 6 bytes of the ATR from position 6
System.arraycopy(atr, 6, startupInfo, 1, 6);
isRatificationOnDeselectSupported = true;
}
/**
* Initializes or post-initializes the object with the application FCI data.
*
* @param selectApplicationResponse The select application response.
* @throws IllegalArgumentException If the FCI is inconsistent.
* @since 2.0.0
*/
private void initializeWithFci(ApduResponseApi selectApplicationResponse)
throws CardCommandException {
this.selectApplicationResponse = selectApplicationResponse;
if (selectApplicationResponse.getDataOut().length == 0) {
// No FCI provided. May be filled later with a Get Data response.
return;
}
// Parse card FCI - to retrieve DF Name (AID), Serial Number, & StartupInfo
// CL-SEL-TLVSTRUC.1
CommandGetDataFci cmdCardGetDataFci =
new CommandGetDataFci(new TransactionContextDto(), new CommandContextDto(false, false));
cmdCardGetDataFci.parseResponseForSelection(selectApplicationResponse, this);
if (!cmdCardGetDataFci.isValidCalypsoFCI()) {
throw new IllegalArgumentException("Bad FCI format");
}
}
/**
* Initializes or post-initializes the object with the application FCI data.
*
* @param cmdCardGetDataFci The command containing the parsed FCI data.
* @throws IllegalArgumentException If the FCI is inconsistent.
* @since 2.2.3
*/
void initializeWithFci(CommandGetDataFci cmdCardGetDataFci) {
isDfInvalidated = cmdCardGetDataFci.isDfInvalidated();
// CL-SEL-DATA.1
dfName = cmdCardGetDataFci.getDfName();
calypsoSerialNumber = cmdCardGetDataFci.getApplicationSerialNumber();
// CL-SI-OTHER.1
startupInfo = cmdCardGetDataFci.getDiscretionaryData();
// CL-SI-ATRFU.1
// CL-SI-ATPRIME.1
// CL-SI-ATB6B5.1
// CL-SI-ATLIGHT.1
// CL-SI-ATBASIC.1
applicationType = startupInfo[SI_APPLICATION_TYPE];
productType = computeProductType(applicationType & 0xFF);
// CL-SI-ASRFU.1
applicationSubType = startupInfo[SI_APPLICATION_SUBTYPE];
if (applicationSubType == (byte) 0x00 || applicationSubType == (byte) 0xFF) {
throw new IllegalArgumentException(
"Unexpected application subtype: " + HexUtil.toHex(applicationSubType));
}
sessionModification = startupInfo[SI_BUFFER_SIZE_INDICATOR];
if (productType == ProductType.PRIME_REVISION_2) {
calypsoCardClass = CalypsoCardClass.LEGACY;
// old cards have their modification counter expressed in number of commands
isModificationCounterInBytes = false;
modificationsCounterMax = REV2_CARD_DEFAULT_WRITE_OPERATIONS_NUMBER_SUPPORTED_PER_SESSION;
} else if (productType == ProductType.BASIC) {
// CL-SI-SMBASIC.1
if (sessionModification < 0x04 || sessionModification > 0x37) {
throw new IllegalArgumentException(
"Wrong session modification value for a Basic type (should be between 04h and 37h): "
+ HexUtil.toHex(sessionModification));
}
calypsoCardClass = CalypsoCardClass.ISO;
isModificationCounterInBytes = false;
modificationsCounterMax = 4; // 3 generic + 1 counter modification
} else {
calypsoCardClass = CalypsoCardClass.ISO;
// session buffer size
// CL-SI-SM.1
if (sessionModification < (byte) 0x06 || sessionModification > (byte) 0x37) {
throw new IllegalArgumentException(
"Session modifications byte should be in range 06h to 47h. Was: "
+ HexUtil.toHex(sessionModification));
}
modificationsCounterMax = BUFFER_SIZE_INDICATOR_TO_BUFFER_SIZE[sessionModification];
}
// CL-SI-ATOPT.1
if (productType == ProductType.PRIME_REVISION_3) {
isExtendedModeSupported = (applicationType & APP_TYPE_CALYPSO_REV_32_MODE) != 0;
isRatificationOnDeselectSupported =
(applicationType & APP_TYPE_RATIFICATION_COMMAND_REQUIRED) == 0;
isPkiModeSupported = (applicationType & APP_TYPE_WITH_PUBLIC_AUTHENTICATION) != 0;
}
if (productType == ProductType.PRIME_REVISION_3
|| productType == ProductType.PRIME_REVISION_2) {
isSvFeatureAvailable = (applicationType & APP_TYPE_WITH_CALYPSO_SV) != 0;
isPinFeatureAvailable = (applicationType & APP_TYPE_WITH_CALYPSO_PIN) != 0;
}
isHce = (calypsoSerialNumber[3] & (byte) 0x80) == (byte) 0x80;
applyPatchIfNeeded();
}
/**
* Some cards have specific features that need to be taken into account. This method identifies
* them and applies the necessary modifications.
*/
private void applyPatchIfNeeded() {
long startupInfoLong = ByteArrayUtil.extractLong(startupInfo, 0, startupInfo.length, false);
if (productType == ProductType.PRIME_REVISION_3) {
applyPatchIfNeededForRevision(patchesRev3, startupInfoLong);
} else if (productType == ProductType.PRIME_REVISION_2
|| productType == ProductType.PRIME_REVISION_1) {
payloadCapacity = 128;
applyPatchIfNeededForRevision(patchesRev12, startupInfoLong);
}
}
private void applyPatchIfNeededForRevision(List extends Patch> patches, long startupInfoLong) {
for (Patch patch : patches) {
if (patch.isApplicableTo(startupInfoLong)) {
patch.apply(this);
return;
}
}
}
/**
* Resolve the card product type from the application type byte
*
* @param applicationType The application type (field of startup info).
* @return The product type.
*/
private ProductType computeProductType(int applicationType) {
if (applicationType == 0) {
throw new IllegalArgumentException("Invalid application type 00h");
}
if (applicationType == 0xFF) {
return ProductType.UNKNOWN;
}
if (applicationType <= 0x1F) {
return ProductType.PRIME_REVISION_2;
}
if (applicationType >= 0x90 && applicationType <= 0x97) {
return ProductType.LIGHT;
}
if (applicationType >= 0x98 && applicationType <= 0x9F) {
return ProductType.BASIC;
}
return ProductType.PRIME_REVISION_3;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public ProductType getProductType() {
return productType;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isHce() {
return isHce;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isDfInvalidated() {
return isDfInvalidated;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte[] getDfName() {
return dfName;
}
/**
* Gets the full Calypso serial number including the possible validity date information in the two
* MSB.
*
*
The serial number to be used as diversifier for key derivation.
* This is the complete number returned by the card in its response to the Select command.
*
* @return A byte array containing the Calypso Serial Number (8 bytes)
* @since 2.0.0
*/
byte[] getCalypsoSerialNumberFull() {
return calypsoSerialNumber;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte[] getApplicationSerialNumber() {
byte[] applicationSerialNumber = calypsoSerialNumber.clone();
applicationSerialNumber[0] = 0;
applicationSerialNumber[1] = 0;
return applicationSerialNumber;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte[] getStartupInfoRawData() {
return startupInfo;
}
/**
* Gets the maximum length of data that an APDU in this card can carry.
*
* @return An int
* @since 2.0.0
*/
int getPayloadCapacity() {
return payloadCapacity;
}
/**
* Tells if the change counter allowed in session is established in number of operations or number
* of bytes modified.
*
*
This varies depending on the product type of the card.
*
* @return True if the counter is number of bytes
* @since 2.0.0
*/
boolean isModificationsCounterInBytes() {
return isModificationCounterInBytes;
}
/**
* Indicates the maximum number of changes allowed in session.
*
*
This number can be a number of operations or a number of commands (see
* isModificationsCounterInBytes)
*
* @return The maximum number of modifications allowed
* @since 2.0.0
*/
int getModificationsCounter() {
return modificationsCounterMax;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getPlatform() {
return startupInfo[SI_PLATFORM];
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getApplicationType() {
return applicationType;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isExtendedModeSupported() {
return isExtendedModeSupported;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isRatificationOnDeselectSupported() {
return isRatificationOnDeselectSupported;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isSvFeatureAvailable() {
return isSvFeatureAvailable;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isPinFeatureAvailable() {
return isPinFeatureAvailable;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isPkiModeSupported() {
return isPkiModeSupported;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getApplicationSubtype() {
return applicationSubType;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getSoftwareIssuer() {
return startupInfo[SI_SOFTWARE_ISSUER];
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getSoftwareVersion() {
return startupInfo[SI_SOFTWARE_VERSION];
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getSoftwareRevision() {
return startupInfo[SI_SOFTWARE_REVISION];
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte getSessionModification() {
return sessionModification;
}
/**
* {@inheritDoc}
*
* @since 2.1.0
*/
@Override
public byte[] getTraceabilityInformation() {
return traceabilityInformation != null ? traceabilityInformation : new byte[0];
}
/**
* {@inheritDoc}
*
* @since 3.1.0
*/
@Override
public byte[] getCardPublicKey() {
return cardPublicKey != null ? cardPublicKey : new byte[0];
}
/**
* {@inheritDoc}
*
* @since 3.1.0
*/
@Override
public byte[] getCardCertificate() {
return cardCertificate != null ? cardCertificate.array() : new byte[0];
}
/**
* {@inheritDoc}
*
* @since 3.1.0
*/
@Override
public byte[] getCaCertificate() {
return caCertificate != null ? caCertificate.array() : new byte[0];
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isDfRatified() {
if (isDfRatified != null) {
return isDfRatified;
}
throw new IllegalStateException(
"Unable to determine the ratification status. No session was opened");
}
/**
* {@inheritDoc}
*
* @since 2.1.1
*/
@Override
public int getTransactionCounter() {
if (transactionCounter == null) {
throw new IllegalStateException(
"Unable to determine the transaction counter. No session was opened");
}
return transactionCounter;
}
/**
* Sets the Stored Value data from the SV Get command
*
* @param svKvc The KVC value.
* @param svGetHeader A not empty array.
* @param svGetData A not empty array.
* @param svBalance the current SV balance.
* @param svLastTNum the last SV transaction number.
* @since 2.0.0
*/
void setSvData(byte svKvc, byte[] svGetHeader, byte[] svGetData, int svBalance, int svLastTNum) {
this.svKvc = svKvc;
this.svGetHeader = svGetHeader;
this.svGetData = svGetData;
this.svBalance = svBalance;
this.svLastTNum = svLastTNum;
}
/**
* Updates the Stored Value data from the SV Get command
*
* @param svBalance the current SV balance.
* @param svLastTNum the last SV transaction number.
* @since 2.3.3
*/
void updateSvData(int svBalance, int svLastTNum) {
this.svBalance = svBalance;
this.svLastTNum = svLastTNum;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public int getSvBalance() {
if (svBalance == null) {
throw new IllegalStateException("No SV Get command has been executed");
}
return svBalance;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public int getSvLastTNum() {
if (svBalance == null) {
throw new IllegalStateException("No SV Get command has been executed");
}
return svLastTNum;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public SvLoadLogRecord getSvLoadLogRecord() {
// try to get it from the file data
ElementaryFile ef = getFileBySfi(CalypsoCardConstant.SV_RELOAD_LOG_FILE_SFI);
if (ef != null) {
byte[] logRecord = ef.getData().getContent();
return new SvLoadLogRecordAdapter(logRecord, 0);
}
return null;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public SvDebitLogRecord getSvDebitLogLastRecord() {
// try to get it from the file data
List svDebitLogRecords = getSvDebitLogAllRecords();
if (!svDebitLogRecords.isEmpty()) {
return svDebitLogRecords.get(0);
}
return null;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public List getSvDebitLogAllRecords() {
List svDebitLogRecords = new ArrayList<>();
// get the logs from the file data
ElementaryFile ef = getFileBySfi(CalypsoCardConstant.SV_DEBIT_LOG_FILE_SFI);
if (ef == null) {
return svDebitLogRecords;
}
SortedMap logRecords = ef.getData().getAllRecordsContent();
for (Map.Entry entry : logRecords.entrySet()) {
svDebitLogRecords.add(new SvDebitLogRecordAdapter(entry.getValue(), 0));
}
return svDebitLogRecords;
}
/**
* Sets the ratification status
*
* @param dfRatified true if the session was ratified.
* @since 2.0.0
*/
void setDfRatified(boolean dfRatified) {
isDfRatified = dfRatified;
}
/**
* Sets the transaction counter.
*
* @param transactionCounter The counter value.
* @since 2.1.1
*/
void setTransactionCounter(int transactionCounter) {
this.transactionCounter = transactionCounter;
}
/**
* Gets the current card class.
*
* @return A not null reference.
* @since 2.0.0
*/
CalypsoCardClass getCardClass() {
return calypsoCardClass;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public DirectoryHeader getDirectoryHeader() {
return directoryHeader;
}
/**
* Sets the DF metadata.
* Updates the invalidation flag.
*
* @param directoryHeader the DF metadata (should be not null).
* @return the current instance.
* @since 2.0.0
*/
CalypsoCard setDirectoryHeader(DirectoryHeader directoryHeader) {
this.directoryHeader = directoryHeader;
this.isDfInvalidated = (directoryHeader.getDfStatus() & (byte) 0x01) != 0;
return this;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public ElementaryFile getFileBySfi(byte sfi) {
if (sfi == 0) {
return null;
}
for (ElementaryFile ef : files) {
if (ef.getSfi() == sfi) {
return ef;
}
}
logger.warn("EF not found (sfi {}h)", HexUtil.toHex(sfi));
return null;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public ElementaryFile getFileByLid(short lid) {
for (ElementaryFile ef : files) {
if (ef.getHeader() != null && ef.getHeader().getLid() == lid) {
return ef;
}
}
logger.warn("EF not found (lid {}h)", HexUtil.toHex(lid));
return null;
}
/**
* {@inheritDoc}
*
* @since 2.1.0
*/
@Override
public Set getFiles() {
return files;
}
/**
* Returns a reference to the currently selected EF.
* If the file having the provided non-zero SFI or LID does not exist, then a new EF is created.
*
* If the SFI and LID are both equal to 0, then the previously selected EF is returned.
*
* @param sfi The SFI (0 if not specified in the current command).
* @param lid The LID (0 if not specified in the current command).
* @return a not null reference.
*/
private ElementaryFileAdapter getOrCreateFile(byte sfi, short lid) {
if (sfi == 0 && lid == 0 && currentEf != null) {
return currentEf;
}
if (sfi != 0) {
// Search by SFI
for (ElementaryFile ef : files) {
if (ef.getSfi() == sfi) {
currentEf = (ElementaryFileAdapter) ef;
return currentEf;
}
}
} else if (lid != 0) {
// Search by LID
for (ElementaryFile ef : files) {
if (ef.getHeader() != null && ef.getHeader().getLid() == lid) {
currentEf = (ElementaryFileAdapter) ef;
return currentEf;
}
}
}
// Create a new EF with the provided SFI
currentEf = new ElementaryFileAdapter(sfi);
files.add(currentEf);
return currentEf;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public boolean isPinBlocked() {
return getPinAttemptRemaining() == 0;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public int getPinAttemptRemaining() {
if (pinAttemptCounter == null) {
throw new IllegalStateException("PIN status has not been checked");
}
return pinAttemptCounter;
}
/**
* Sets the PIN attempts counter.
* The PIN attempt counter is interpreted to give the results of the methods {@link #isPinBlocked}
* and {@link #getPinAttemptRemaining}.
*
* @param pinAttemptCounter the number of remaining attempts to present the PIN code.
* @since 2.0.0
*/
void setPinAttemptRemaining(int pinAttemptCounter) {
this.pinAttemptCounter = pinAttemptCounter;
}
/**
* Sets the provided {@link FileHeaderAdapter} to the current selected file.
* If EF does not exist, then it is created.
*
* @param sfi the SFI.
* @param header the file header (should be not null).
* @since 2.0.0
*/
void setFileHeader(byte sfi, FileHeaderAdapter header) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, header.getLid());
if (ef.getHeader() == null) {
ef.setHeader(header);
} else {
ef.getHeader().updateMissingInfoFrom(header);
}
}
/**
* Set or replace the entire content of the specified record #numRecord of the current selected
* file by the provided content.
* If EF does not exist, then it is created.
*
* @param sfi the SFI.
* @param numRecord the record number (should be {@code >=} 1).
* @param content the content (should be not empty).
* @since 2.0.0
*/
void setContent(byte sfi, int numRecord, byte[] content) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, (short) 0);
ef.getData().setContent(numRecord, content);
}
/**
* Sets a counter value in record #1 of the current selected file.
* If EF does not exist, then it is created.
*
* @param sfi the SFI.
* @param numCounter the counter number (should be {@code >=} 1).
* @param content the counter value (should be not null and 3 bytes length).
* @since 2.0.0
*/
void setCounter(byte sfi, int numCounter, byte[] content) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, (short) 0);
ef.getData().setCounter(numCounter, content);
}
/**
* Set or replace the content at the specified offset of record #numRecord of the current selected
* file by a copy of the provided content.
* If EF does not exist, then it is created.
* If actual record content is not set or has a size {@code <} offset, then missing data will be
* padded with 0.
*
* @param sfi the SFI.
* @param numRecord the record number (should be {@code >=} 1).
* @param content the content (should be not empty).
* @param offset the offset (should be {@code >=} 0).
* @since 2.0.0
*/
void setContent(byte sfi, int numRecord, byte[] content, int offset) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, (short) 0);
ef.getData().setContent(numRecord, content, offset);
}
/**
* Fills the content at the specified offset of the specified record of the current selected file
* using a binary OR operation with the provided content.
* If EF does not exist, then it is created.
* If actual record content is not set or has a size {@code <} offset + content size, then missing
* data will be completed by the provided content.
*
* @param sfi the SFI.
* @param numRecord the record number (should be {@code >=} 1).
* @param content the content (should be not empty).
* @since 2.1.0
*/
void fillContent(byte sfi, int numRecord, byte[] content, int offset) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, (short) 0);
ef.getData().fillContent(numRecord, content, offset);
}
/**
* Add cyclic content at record #1 by rolling previously all actual records contents (record #1 ->
* record #2, record #2 -> record #3,...) of the current selected file.
* This is useful for cyclic files. Note that records are infinitely shifted.
*
* If EF does not exist, then it is created.
*
* @param sfi the SFI.
* @param content the content (should be not empty).
* @since 2.0.0
*/
void addCyclicContent(byte sfi, byte[] content) {
ElementaryFileAdapter ef = getOrCreateFile(sfi, (short) 0);
ef.getData().addCyclicContent(content);
}
/**
* Make a backup of the Elementary Files.
* This method should be used before starting a card secure session.
*
* @since 2.0.0
*/
void backupFiles() {
copyFiles(files, filesBackup);
svBalanceBackup = svBalance;
svLastTNumBackup = svLastTNum;
}
/**
* Restore the last backup of Elementary Files.
* This method should be used when SW of the card close secure session command is unsuccessful or
* if secure session is aborted.
*
* @since 2.0.0
*/
void restoreFiles() {
copyFiles(filesBackup, files);
svBalance = svBalanceBackup;
svLastTNum = svLastTNumBackup;
}
/**
* Copy a set of ElementaryFile to another one by cloning each element.
*
* @param src the source (should be not null).
* @param dest the destination (should be not null).
*/
private static void copyFiles(Set src, Set dest) {
dest.clear();
for (ElementaryFile file : src) {
dest.add(new ElementaryFileAdapter(file));
}
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public String getPowerOnData() {
return powerOnData;
}
/**
* {@inheritDoc}
*
* @since 2.0.0
*/
@Override
public byte[] getSelectApplicationResponse() {
if (selectApplicationResponse == null) {
return new byte[0];
}
return selectApplicationResponse.getApdu();
}
/**
* Sets the DF invalidation status.
*
* @param isInvalidated true if DF is invalidated, false if DF is rehabilitated.
* @since 2.3.7
*/
void setDfInvalidated(boolean isInvalidated) {
this.isDfInvalidated = isInvalidated;
}
/**
* Sets the challenge received in response to the GET CHALLENGE command.
*
* @param challenge A not empty array.
* @since 2.0.0
*/
void setChallenge(byte[] challenge) {
this.challenge = challenge;
}
/**
* Sets the traceability information received in response to the GET DATA command for the tag
* {@link org.eclipse.keypop.calypso.card.GetDataTag#TRACEABILITY_INFORMATION}.
*
* @param traceabilityInformation The traceability information.
* @since 2.1.0
*/
void setTraceabilityInformation(byte[] traceabilityInformation) {
this.traceabilityInformation = traceabilityInformation;
}
/**
* Sets the card public key received in response to the GET DATA command for the tag {@link
* org.eclipse.keypop.calypso.card.GetDataTag#CARD_PUBLIC_KEY}.
*
* @param cardPublicKey The card public key.
* @since 3.1.0
*/
void setCardPublicKey(byte[] cardPublicKey) {
this.cardPublicKey = cardPublicKey;
}
/**
* Sets the card public key retrieved from the card certificate.
*
* @param cardPublicKeySpi The card public key SPI.
* @since 3.1.0
*/
void setCardPublicKeySpi(CardPublicKeySpi cardPublicKeySpi) {
this.cardPublicKeySpi = cardPublicKeySpi;
}
/**
* Retrieves the card public key SPI retrieved from the card certificate.
*
* Note that the card public key obtained with a Get Data command will not be available with
* this method.
*
* @return The card public key SPI or null if not available.
* @since 3.1.0
*/
CardPublicKeySpi getCardPublicKeySpi() {
return cardPublicKeySpi;
}
/**
* Adds the card certificate bytes received in response to the GET DATA command for the tag {@link
* org.eclipse.keypop.calypso.card.GetDataTag#CARD_CERTIFICATE}.
*
* @param cardCertificateBytes The card certificate bytes.
* @param isFirstPart true when the provided data is the first part of the certificate.
* @since 3.1.0
*/
void addCardCertificateBytes(byte[] cardCertificateBytes, boolean isFirstPart) {
if (isFirstPart) {
this.cardCertificate = ByteBuffer.allocate(CalypsoCardConstant.CARD_CERTIFICATE_SIZE);
}
cardCertificate.put(cardCertificateBytes);
}
/**
* Sets the CA certificate bytes received in response to the GET DATA command for the tag {@link
* org.eclipse.keypop.calypso.card.GetDataTag#CA_CERTIFICATE}.
*
* @param caCertificateBytes The CA certificate bytes.
* @param isFirstPart true when the provided data is the first part of the certificate.
* @since 3.1.0
*/
void addCaCertificateBytes(byte[] caCertificateBytes, boolean isFirstPart) {
if (isFirstPart) {
this.caCertificate = ByteBuffer.allocate(CalypsoCardConstant.CA_CERTIFICATE_SIZE);
}
caCertificate.put(caCertificateBytes);
}
/**
* Sets the SV signature.
*
* @param svOperationSignature A not empty array.
* @since 2.0.0
*/
void setSvOperationSignature(byte[] svOperationSignature) {
this.svOperationSignature = svOperationSignature;
}
/**
* Gets the challenge received from the card
*
* @return An array of bytes containing the challenge bytes (variable length according to the
* product type of the card). May be null if the challenge is not available.
* @since 2.0.0
*/
byte[] getChallenge() {
return challenge;
}
/**
* Gets the SV KVC from the card
*
* @return The SV KVC byte.
* @since 2.0.0
*/
byte getSvKvc() {
return svKvc;
}
/**
* Gets the SV Get command header
*
* @return A byte array containing the SV Get command header.
* @throws IllegalStateException If the requested data has not been set.
* @since 2.0.0
*/
byte[] getSvGetHeader() {
if (svGetHeader == null) {
throw new IllegalStateException("SV Get Header not available");
}
return svGetHeader;
}
/**
* Gets the SV Get command response data
*
* @return A byte array containing the SV Get command response data.
* @throws IllegalStateException If the requested data has not been set.
* @since 2.0.0
*/
byte[] getSvGetData() {
if (svGetData == null) {
throw new IllegalStateException("SV Get Data not available");
}
return svGetData;
}
/**
* Gets the last SV Operation signature (SV Reload, Debit or Undebit)
*
* @return A byte array containing the SV Operation signature or null if not available.
* @since 2.0.0
*/
byte[] getSvOperationSignature() {
return svOperationSignature;
}
/**
* Indicates if the response of the Increase/Decrease counter command is postponed to the close
* secure session (old revision 2 cards).
*
* @return true if the response of the Increase/Decrease counter command is postponed.
* @since 2.2.4
*/
boolean isCounterValuePostponed() {
return isCounterValuePostponed;
}
/**
* Indicates if the card is of a type corresponding to the specific case 1.
*
* @return true if the card corresponds to the specific case 1, false otherwise.
* @see #patchesRev12
* @since 2.3.5
*/
boolean isLegacyCase1() {
return isLegacyCase1;
}
/**
* Disables extended mode. Although the card is in revision 3.2, it has indicated in response to
* the "Open Secure Session" command that it does not use AES keys.
*
* @since 2.3.1
*/
void disableExtendedMode() {
isExtendedModeSupported = false;
}
WriteAccessLevel getPreOpenWriteAccessLevel() {
return preOpenWriteAccessLevel;
}
CalypsoCardAdapter setPreOpenWriteAccessLevel(WriteAccessLevel preOpenWriteAccessLevel) {
this.preOpenWriteAccessLevel = preOpenWriteAccessLevel;
return this;
}
byte[] getPreOpenDataOut() {
return preOpenDataOut;
}
CalypsoCardAdapter setPreOpenDataOut(byte[] preOpenDataOut) {
this.preOpenDataOut = preOpenDataOut;
return this;
}
/**
* Gets the object content as a Json string.
*
* @return A not empty string.
* @since 2.0.0
*/
@Override
public String toString() {
return JsonUtil.toJson(this);
}
/** POJO containing card specificities to be applied according to startup info. */
private abstract static class Patch {
private final long startupInfo;
private final Long mask;
private Patch(String startupInfo, String mask) {
this.startupInfo = HexUtil.toLong(startupInfo);
this.mask = HexUtil.toLong(mask);
}
private boolean isApplicableTo(long startupInfo) {
return mask == null
? this.startupInfo == startupInfo
: this.startupInfo == (startupInfo & mask);
}
abstract void apply(CalypsoCardAdapter calypsoCard);
}
/** POJO containing card rev 3 specificities to be applied according to startup info. */
private static class PatchRev3 extends Patch {
private Integer payloadCapacity;
private PatchRev3(String startupInfo, String mask) {
super(startupInfo, mask);
}
@Override
void apply(CalypsoCardAdapter calypsoCard) {
if (payloadCapacity != null) {
calypsoCard.payloadCapacity = payloadCapacity;
}
}
private PatchRev3 setPayloadCapacity(Integer payloadCapacity) {
this.payloadCapacity = payloadCapacity;
return this;
}
}
/** POJO containing card rev 1 & 2 specificities to be applied according to startup info. */
private static class PatchRev12 extends Patch {
private Boolean isCounterValuePostponed;
private Boolean isLegacyCase1;
private PatchRev12(String startupInfo, String mask) {
super(startupInfo, mask);
}
@Override
void apply(CalypsoCardAdapter calypsoCard) {
if (isCounterValuePostponed != null) {
calypsoCard.isCounterValuePostponed = isCounterValuePostponed;
}
if (isLegacyCase1 != null) {
calypsoCard.isLegacyCase1 = isLegacyCase1;
}
}
private PatchRev12 setCounterValuePostponed() {
isCounterValuePostponed = true;
return this;
}
private PatchRev12 setLegacyCase1() {
isLegacyCase1 = true;
return this;
}
}
}