+ * Used internally for protocol encryption + *
+ * Can be used on subsequent reading to first fast check that + * you read the same card + * + * @return UID byte array + */ + public byte[] GetUID() { + if (mIsoDep == null) return null; + return mIsoDep.getId(); + } + + /** + * Return ISO 14443-3 tag reading timeout + */ + public int getTimeout() { + if (mIsoDep == null) return 60000; + return mIsoDep.getTimeout(); + } + + + /** + * protocolKey + * a base value used to construct the communication encryption key derived from PIN and UID + * See [1] 4.3, 4.4, 4.5 + * {@see run_OpenSession} + */ + private byte[] protocolKey; + + public void resetProtocolKey() { + protocolKey = null; + } + + /** + * Calculate protocolKey + * See [1] 4.3, 4.4, 4.5 + * + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + */ + public void CreateProtocolKey() throws NoSuchAlgorithmException, InvalidKeyException { + protocolKey = CardCrypto.pbkdf2(Util.calculateSHA256(mPIN), GetUID(), 50); + //Log.e("Reader", String.format("PIN: %s, Protocol key: %s", mPIN, Util.bytesToHex(protocolKey))); + if (sessionKey != null) { + sessionKey = null; + } + } + + /** + * current session communication encryption key + * See [1] 4.1, 4.3, 4.4, 4.5 + */ + private byte[] sessionKey = null; + + + /** + * Execute open session command and calculate session key {@see CardProtocol.sessionKey} + * During this command the card and the device exchange with random challenges (for Fast encryption mode) or + * public keys (for Strong encryption mode) and calculate sessionKey based protocolKey as: + * - sha256(challengeDevice|challengeCard|protocolKey) for Fast encryption + * - sha256(ECDH shared secret|protocolKey) for Strong encryption + * See [1] 4.1, 4.3, 4.4, 4.5 + * + * @param encryptionMode - mode of encryption {@link TangemCard.EncryptionMode} + * @throws Exception if something went wrong + */ + private void run_OpenSession(TangemCard.EncryptionMode encryptionMode) throws Exception { + sessionKey = null; + try { + CommandApdu cmdApdu = new CommandApdu(CommandApdu.ISO_CLA, INS.OpenSession.Code, 0, encryptionMode.getP()); + + switch (encryptionMode) { + case Fast: { + // See [1] 4.3 + byte[] baMyChallenge = Util.generateRandomBytes(16); + + cmdApdu.addTLV(TLV.Tag.TAG_Session_Key_A, baMyChallenge); + Log.i(logTag, cmdApdu.getCommandName()); + ResponseApdu rspApdu = null; + + try { + if (mIsoDep == null) { + throw new TangemException_TagLost(); + } + byte[] cmdBytes = cmdApdu.toBytes(); + String cmdStr = CommandApdu.toString(cmdBytes, cmdApdu.getLc()); + Log.v("NFC", String.format("<< [%s]: %s", cmdApdu.getCommandName(), cmdStr)); + + byte[] rsp = mIsoDep.transceive(cmdBytes); + rspApdu = new ResponseApdu(rsp); + + Log.v("NFC", String.format(">> [%s]: %s", cmdApdu.getCommandName(), Util.bytesToHex(rsp))); + + if (rspApdu.isParsedWithError()) { + throw new Exception("Can't parse answer"); + } + } catch (Exception E) { + sessionKey = null; + throw E; + } + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + byte[] baTheirsChallenge = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Session_Key_B).Value; + if (protocolKey == null) CreateProtocolKey(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(baMyChallenge); + outputStream.write(baTheirsChallenge); + outputStream.write(protocolKey); + sessionKey = Util.calculateSHA256(outputStream.toByteArray()); + //Log.i(logTag, String.format("Session key: %s", Util.bytesToHex(sessionKey))); + } else { + Log.e(logTag, String.format("Failed: %04X - %s", rspApdu.getSW1SW2(), rspApdu.getSW1SW2Description())); + throw new Exception(String.format("Can't open session: SW - %04X", rspApdu.getSW1SW2())); + } + } + break; + case Strong: { + // See [1] 4.4 + KeyPairGenerator kpgen = KeyPairGenerator.getInstance("ECDH", "SC"); + kpgen.initialize(new ECGenParameterSpec("secp256k1"), new SecureRandom()); + KeyPair KP = kpgen.generateKeyPair(); + KeyAgreement ka = KeyAgreement.getInstance("ECDH", "SC"); + ka.init(KP.getPrivate()); + + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256k1"); + //return spec.getG().multiply(new BigInteger((ECPrivateKey) )).getEncoded(false); + ECPublicKey eckey = (ECPublicKey) KP.getPublic(); + byte[] baMyPublicKey = eckey.getQ().getEncoded(false); + + cmdApdu.addTLV(TLV.Tag.TAG_Session_Key_A, baMyPublicKey); + Log.i(logTag, cmdApdu.getCommandName()); + ResponseApdu rspApdu = null; + + try { + if (mIsoDep == null) { + throw new TangemException_TagLost(); + } + //mIsoDep.setTimeout(msTimeout); + + byte[] cmdBytes = cmdApdu.toBytes(); + String cmdStr = CommandApdu.toString(cmdBytes, cmdApdu.getLc()); + Log.v("NFC", String.format("<< [%s]: %s", cmdApdu.getCommandName(), cmdStr)); + byte[] rsp = mIsoDep.transceive(cmdBytes); + rspApdu = new ResponseApdu(rsp); + Log.v("NFC", String.format(">> [%s]: %s", cmdApdu.getCommandName(), Util.bytesToHex(rsp))); + + if (rspApdu.isParsedWithError()) { + throw new Exception("Can't parse answer"); + } + } catch (Exception E) { + sessionKey = null; + throw E; + } + + if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) { + Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" "))); + byte[] baTheirsPublicKey = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Session_Key_B).Value; + ka.doPhase(CardCrypto.LoadPublicKey(baTheirsPublicKey), true); + if (protocolKey == null) CreateProtocolKey(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(ka.generateSecret()); + outputStream.write(protocolKey); + sessionKey = Util.calculateSHA256(outputStream.toByteArray()); +// Log.i(logTag, String.format("Session key: %s", Util.bytesToHex(sessionKey))); + } else { + Log.i(logTag, String.format("Failed: %04X - %s", rspApdu.getSW1SW2(), rspApdu.getSW1SW2Description())); + throw new Exception(String.format("Can't open session: SW - %04X", rspApdu.getSW1SW2())); + } + } + break; + default: + throw new Exception("Unknown encryption mode"); + } + } catch (Exception e) { + e.printStackTrace(); + Log.e(logTag, String.format("Exception: %s", e.getMessage())); + throw new Exception("Can't open session: " + e.getMessage()); + } + } + + + /** + * Send the specified APDU command to a card and receive an answer + * Should have a prior opened encryption session if encryption is used + * See [1] 4 + * + * @param cmdApdu - APDU command to send + * @param breakOnNeedPause - Specifies what to do when the card requests a security delay (interrupt transfer or wait till the end of the delay ) + * @return - response APDU + * @throws Exception - if something went wrong + */ + private ResponseApdu SendAndReceive(CommandApdu cmdApdu, boolean breakOnNeedPause) throws Exception { + if (mCard.encryptionMode != TangemCard.EncryptionMode.None) { + if (sessionKey == null) { + run_OpenSession(mCard.encryptionMode); + } + cmdApdu.Crypt(sessionKey); + } + cmdApdu.setP1(mCard.encryptionMode.getP()); + byte[] cmdBytes = cmdApdu.toBytes(); + String cmdStr = CommandApdu.toString(cmdBytes, cmdApdu.getLc()); + Log.v("NFC", String.format("<< [%s]: %s", cmdApdu.getCommandName(), cmdStr)); + byte[] rsp; + ResponseApdu rspApdu; + try { + do { + try { + mNotifications.onReadBeforeRequest(mIsoDep.getTimeout()); + try { + rsp = mIsoDep.transceive(cmdBytes); + } finally { + mNotifications.onReadAfterRequest(); + } + } catch (IOException e) { + if (e.getMessage().contains("length")) { + throw new TangemException_ExtendedLengthNotSupported(e.getMessage()); + } + throw e; + } + if (mCard.encryptionMode != TangemCard.EncryptionMode.None && !ResponseApdu.isStatusWord(rsp, SW.NEED_PAUSE)) { + rspApdu = ResponseApdu.Decrypt(rsp, sessionKey); + } else { + rspApdu = new ResponseApdu(rsp); + } + + if (rspApdu.isParsedWithError()) { + Log.v("NFC", String.format(">> [%s]: %s", cmdApdu.getCommandName(), Util.bytesToHex(rsp))); + throw new TangemException(rspApdu.getParseErroMessage()); + } else if (rspApdu.getSW1SW2() == SW.NEED_PAUSE && mNotifications != null) { + int remainingPause = 60000; + TLV tlvPause = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Pause); + if (tlvPause != null) { + remainingPause = rspApdu.getTLVs().getTagAsInt(TLV.Tag.TAG_Pause) * 10; + } + + Log.v("NFC", String.format(">> Security delay, remaining %f s", remainingPause / 1000.0)); + if (breakOnNeedPause) { + break; + } else { + mNotifications.onReadWait(remainingPause); + } + } else { + Log.v("NFC", String.format(">> [%s]: %s", cmdApdu.getCommandName(), Util.bytesToHex(rsp))); + } + } while (rspApdu.getSW1SW2() == SW.NEED_PAUSE); + } finally { + mNotifications.onReadWait(0); + } + + return rspApdu; + } + + /** + * Helper for preparing APDU command - init CommandApdu object and add common TLV tags ([CID,] PIN) + * + * @param ins - instruction code {@link INS} + * @return CommandAPDU + * @throws NoSuchAlgorithmException - if no sha256 library found + */ + private CommandApdu StartPrepareCommand(INS ins) throws NoSuchAlgorithmException { + CommandApdu Apdu = new CommandApdu(ins); + byte[] baPIN = Util.calculateSHA256(mPIN); + Apdu.addTLV(TLV.Tag.TAG_PIN, baPIN); + if (ins != INS.Read) { + Apdu.addTLV(TLV.Tag.TAG_CardID, mCard.getCID()); + } + return Apdu; + } + +// /** +// * Run READ command and parse answer +// * {@see run_Read(boolean parseResult) } +// * +// * @throws Exception if something went wrong +// */ +// public void run_Read() throws Exception { +// run_Read(true); +// } + + /** + * Run READ command and parse answer (if specified) + *
+ * This command returns all card and wallet data, including unique card number (CID) that has to be submitted when further calling all other commands. Therefore, + * READ_CARD should always be used in the beginning of communication session between NFC device and Tangem card + *
+ * In order to obtain card’s data, the app should call READ_CARD command with correct PIN1 value as a parameter. The card will not respond if wrong PIN1
+ * has been submitted
+ * This command requires only PIN1 parameter while other commands also need CID
+ * See [1] 8, 8.2
+ *
+ * @throws Exception if something went wrong
+ */
+ public void run_Read() throws Exception {
+ CommandApdu rqApdu = StartPrepareCommand(INS.Read);
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ readResult = rspApdu.getTLVs();
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Possible PIN is invalid!\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("FAILED: [%04X]\n", rspApdu.getSW1SW2()));
+ }
+ }
+
+
+ /**
+ * A TLV list containing response of the last READ command
+ */
+ private TLVList readResult = null;
+
+ public void clearReadResult() {
+ sessionKey = null;
+ readResult = null;
+ }
+
+ public boolean haveReadResult() {
+ return readResult != null;
+ }
+
+ public TLVList getReadResult() {
+ return readResult;
+ }
+
+
+ /**
+ * VERIFY_CARD command and verify card's signature
+ * By using standard challenge-response scheme, the card proves
+ * possession of CARD_PRIVATE_KEY that corresponds to CARD_PUBLIC_KEY returned by READ_CARD command
+ * See [1] 3.2, 8.9
+ *
+ * @return TLVList with response in case of success
+ * @throws Exception - if something went wrong
+ */
+ public TLVList run_VerifyCard() throws Exception {
+ if (mCard.getCardPublicKey() == null || readResult == null) {
+ throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+ }
+ if (mCard.getStatus() == TangemCard.Status.NotPersonalized) {
+ getCard().setManufacturer(Manufacturer.Unknown, false);
+ return null;
+ }
+
+ if (mCard.getCardPublicKey() == null) {
+ throw new TangemException("Not all data read, can't verify card!");
+ }
+
+ CommandApdu rqApdu = StartPrepareCommand(INS.VerifyCard);
+ byte[] bChallenge = Util.generateRandomBytes(16);
+ rqApdu.addTLV(TLV.Tag.TAG_Challenge, bChallenge);
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ TLVList verifyResult = rspApdu.getTLVs();
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ verifyResult.add(new TLV(TLV.Tag.TAG_Challenge, bChallenge));
+
+ TLV tlvSalt = verifyResult.getTLV(TLV.Tag.TAG_Salt);
+ TLV tlvCardSignature = verifyResult.getTLV(TLV.Tag.TAG_CardSignature);
+
+ if (tlvSalt == null || tlvCardSignature == null) {
+ throw new TangemException("Not all data read, can't verify card!");
+ }
+
+ try {
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ bs.write(bChallenge);
+ bs.write(tlvSalt.Value);
+ byte[] dataArray = bs.toByteArray();
+ if (CardCrypto.VerifySignature(mCard.getCardPublicKey(), dataArray, tlvCardSignature.Value)) {
+ getCard().setCardPublicKeyValid(true);
+ Log.i(logTag, "Card signature verification OK");
+ } else {
+ Log.e(logTag, "Card signature verification FAILED");
+ getCard().setCardPublicKeyValid(false);
+ }
+
+ getCard().setManufacturer(Manufacturer.FindManufacturer(readResult.getTLV(TLV.Tag.TAG_Manufacture_ID).getAsString()), true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ getCard().setManufacturer(Manufacturer.Unknown, false);
+ }
+ return verifyResult;
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Invalid PIN\n", rspApdu.getSW1SW2()));
+ } else {
+ getCard().setManufacturer(Manufacturer.Unknown, false);
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+
+ }
+
+ /**
+ * CREATE_WALLET command
+ * This command will create a new wallet on the card having ‘Empty’ state. A key pair WALLET_PUBLIC_KEY / WALLET_PRIVATE_KEY is generated and securely stored in
+ * the card.
+ * See [1] 3.4, 8.3
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @throws Exception - if something went wrong
+ */
+ public void run_CreateWallet(String PIN2) throws Exception {
+ if (readResult == null) throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+ CommandApdu rqApdu = StartPrepareCommand(INS.CreateWallet);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ }
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Invalid PIN\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * CHECK_WALLET command without signature verification
+ * Card will sign a challenge to prove that it possesses WALLET_PRIVATE_KEY corresponding to WALLET_PUBLIC_KEY. Standard challenge/response scheme is used.
+ * See [1] 3.4, 8.4
+ *
+ * @return TLVList from answer in case of success
+ * @throws Exception - if something went wrong
+ */
+ private TLVList run_CheckWallet() throws Exception {
+ CommandApdu rqApdu = StartPrepareCommand(INS.CheckWallet);
+ byte[] bChallenge = Util.generateRandomBytes(16);
+ rqApdu.addTLV(TLV.Tag.TAG_Challenge, bChallenge);
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ TLVList Result = rspApdu.getTLVs();
+ Result.add(new TLV(TLV.Tag.TAG_Challenge, bChallenge));
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ return Result;
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Invalid PIN\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * CHECK_WALLET command and verify wallet's signature
+ * Card will sign a challenge to prove that it possesses WALLET_PRIVATE_KEY corresponding to WALLET_PUBLIC_KEY. Standard challenge/response scheme is used.
+ * It will first run READ command if no reads where made before. Then execute CHECK_WALLET command and verify signature in the response.
+ * Set WalletPublicKeyValid in {@link TangemCard}
+ * See [1] 3.4, 8.4
+ *
+ * @throws Exception - if something went wrong
+ */
+ public void run_CheckWalletWithSignatureVerify() throws Exception {
+ if (mCard.getCardPublicKey() == null || readResult == null) {
+ throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+ }
+ if (mCard.getStatus() == TangemCard.Status.NotPersonalized) {
+ getCard().setManufacturer(Manufacturer.Unknown, false);
+ return;
+ }
+
+ if (readResult.getTagAsInt(TLV.Tag.TAG_Status) != TangemCard.Status.Loaded.getCode()) {
+ throw new TangemException("Card must be loaded");
+ }
+ TLVList checkResult = run_CheckWallet();
+ if (checkResult == null) return;
+
+ TLV tlvCurveID = readResult.getTLV(TLV.Tag.TAG_CurveID);
+ TLV tlvPublicKey = readResult.getTLV(TLV.Tag.TAG_Wallet_PublicKey);
+ TLV tlvChallenge = checkResult.getTLV(TLV.Tag.TAG_Challenge);
+ TLV tlvSalt = checkResult.getTLV(TLV.Tag.TAG_Salt);
+ TLV tlvSignature = checkResult.getTLV(TLV.Tag.TAG_Signature);
+
+ if (tlvCurveID == null || tlvPublicKey == null || tlvChallenge == null || tlvSalt == null || tlvSignature == null) {
+ throw new TangemException("Not all data read, can't check signature!");
+ }
+
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ bs.write(tlvChallenge.Value);
+ bs.write(tlvSalt.Value);
+ byte[] dataArray = bs.toByteArray();
+
+ if (CardCrypto.VerifySignature(tlvCurveID.getAsString(), tlvPublicKey.Value, dataArray, tlvSignature.Value)) {
+ Log.i(logTag, "Signature verification OK");
+ mCard.setWalletPublicKeyValid(true);
+ } else {
+ mCard.setWalletPublicKeyValid(false);
+ }
+ }
+
+ /**
+ * PURGE_WALLET command
+ * See [1] 3.4, 8.11
+ * This command deletes all wallet data. If Is_Reusable flag is enabled during personalization, the card changes state to ‘Empty’ and a new wallet can be created by
+ * CREATE_WALLET command. If Is_Reusable flag is disabled, the card switches to ‘Purged’ state. ‘Purged’ state is final, it makes the card useless.
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @throws Exception - if something went wrong
+ */
+ public void run_PurgeWallet(String PIN2) throws Exception {
+ CommandApdu rqApdu = StartPrepareCommand(INS.PurgeWallet);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ }
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Invalid PIN\n", rspApdu.getSW1SW2()));
+ } else {
+
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * Execute SET_PIN command
+ * This command changes PIN1 and PIN2 passwords if it is allowed by Allow_SET_PIN1 and Allow_SET_PIN2 flags in Settings_Mask. Host application can submit
+ * unchanged passwords (New_PIN1 = PIN1 and New_PIN2 = PIN2) in order to check its correctness. Depending on the result, Status_Word in the command response will have
+ * these values:
+ * SW_PINS_NOT_CHANGED = 0x9000
+ * SW_PIN1_CHANGED = 0x9001
+ * SW_PIN2_CHANGED = 0x9002
+ * SW_PINS_CHANGED = 0x9003
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @param newPin - new value of PIN code
+ * @param newPin2 - new value of PIN2 code
+ * @param breakOnNeedPause - flag that specify what
+ * @throws Exception - if something went wrong
+ */
+ public void run_SetPIN(String PIN2, String newPin, String newPin2, boolean breakOnNeedPause) throws Exception {
+ CommandApdu rqApdu = StartPrepareCommand(INS.SwapPIN);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+ rqApdu.addTLV(TLV.Tag.TAG_NewPIN, Util.calculateSHA256(newPin));
+ rqApdu.addTLV(TLV.Tag.TAG_NewPIN2, Util.calculateSHA256(newPin2));
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, breakOnNeedPause);
+
+ if (rspApdu.isStatus(SW.PIN1_CHANGED) || rspApdu.isStatus(SW.PIN2_CHANGED) || rspApdu.isStatus(SW.PINS_CHANGED) || rspApdu.isStatus(SW.PINS_NOT_CHANGED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (newPin2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ } else {
+ mCard.setUseDefaultPIN2(false);
+ }
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Invalid PIN\n", rspApdu.getSW1SW2()));
+ } else if (breakOnNeedPause && rspApdu.isStatus(SW.NEED_PAUSE)) {
+ throw new TangemException_NeedPause(String.format("FAILED: [%04X] - Need pause\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * SIGN command to sign hashes - SigningMethod=0,2,4 (see {@link TangemCard.SigningMethod})
+ * See [1] 8.6
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @param hashes - array of digests to sign (max 10 digest at a time)
+ * @param issuerTransactionSignature - signature of hashes, if card need issuer validation before sign (for SigningMethod=2)
+ * @param issuerData - new issuerData to write on card (only for SigningMethod=4, null for other)
+ * @param issuerDataSignature - signature of issuerData, if issuerData specified(for SigningMethod=4)
+ * @return TLVList with card answer contained wallet signatures of digests from hashes array (in case of success)
+ * @throws Exception - if something went wrong
+ */
+ public TLVList run_SignHashes(String PIN2, byte[][] hashes, byte[] issuerTransactionSignature, byte[] issuerData, byte[] issuerDataSignature) throws Exception {
+ if (mCard.getSigningMethod() != TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer && mCard.getSigningMethod() != TangemCard.SigningMethod.Sign_Hash) {
+ throw new TangemException("Card don't support signing hashes!");
+ }
+
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ if (hashes.length > 10) throw new TangemException("To much hashes in one transaction!");
+ for (int i = 0; i < hashes.length; i++) {
+ if (i != 0 && hashes[0].length != hashes[i].length)
+ throw new TangemException("Hashes length must be identical!");
+ bs.write(hashes[i]);
+ }
+ CommandApdu rqApdu = StartPrepareCommand(INS.Sign);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+ rqApdu.addTLV_U8(TLV.Tag.TAG_TrOut_HashSize, hashes[0].length);
+ rqApdu.addTLV(TLV.Tag.TAG_TrOut_Hash, bs.toByteArray());
+ if (issuerData != null) {
+ if (mCard.getSigningMethod() != TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer_And_WriteIssuerData)
+ throw new TangemException("Card don't support simultaneous sign with write issuer data!");
+
+ if (issuerDataSignature == null)
+ throw new TangemException("Card require issuer validation before write issuer data");
+ bs.write(issuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, issuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
+ }
+ if (issuerTransactionSignature != null) {
+ //byte[] issuerSignature = CardCrypto.Signature(issuer.getPrivateTransactionKey(), bs.toByteArray());
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Transaction_Signature, issuerTransactionSignature);
+ } else if (mCard.getSigningMethod() == TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer) {
+ throw new TangemException("Card require issuer validation before sign the transaction!");
+ }
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ TLVList Result = rspApdu.getTLVs();
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ }
+ return Result;
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Possible the PIN or PIN2 is invalid!\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * SIGN raw tx - SigningMethod=1 (see {@link TangemCard.SigningMethod})
+ * See [1] 8.6
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @param hashAlgID - name of hash alg, used for signature
+ * @param bTxOutData - part of raw transaction to sign
+ * @param issuerTransactionSignature - signature of hashes, if card need issuer validation before sign (for SigningMethod=2)
+ * @param issuerData - new issuerData to write on card (only for SigningMethod=4, null for other)
+ * @param issuerDataSignature - signature of issuerData, if issuerData specified(for SigningMethod=4)
+ * @return TLVList with card answer contained wallet signatures of bTxOutData(in case of success)
+ * @throws Exception - if something went wrong
+ */
+ public TLVList run_SignRaw(String PIN2, String hashAlgID, byte[] bTxOutData, byte[] issuerTransactionSignature, byte[] issuerData, byte[] issuerDataSignature) throws Exception {
+
+ CommandApdu rqApdu = StartPrepareCommand(INS.Sign);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+ rqApdu.addTLV(TLV.Tag.TAG_TrOut_Raw, bTxOutData);
+ rqApdu.addTLV(TLV.Tag.TAG_HashAlgID, hashAlgID.getBytes("US-ASCII"));
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ if (bTxOutData.length > 1024) throw new TangemException("Raw transaction size is to big!");
+ bs.write(bTxOutData);
+
+ if (issuerData != null) {
+ if (mCard.getSigningMethod() != TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer_And_WriteIssuerData)
+ throw new TangemException("Card don't support simultaneous sign with write issuer data!");
+
+ if (issuerDataSignature == null)
+ throw new TangemException("Card require issuer validation before write issuer data");
+ bs.write(issuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, issuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
+ }
+ if (issuerTransactionSignature != null) {
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Transaction_Signature, issuerTransactionSignature);
+ } else if (mCard.getSigningMethod() == TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer) {
+ throw new TangemException("Card require issuer validation before sign the transaction!");
+ }
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ TLVList Result = rspApdu.getTLVs();
+ Result.add(new TLV(TLV.Tag.TAG_TrOut_Raw, bTxOutData));
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ }
+ return Result;
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Possible the PIN or PIN2 is invalid!\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * VERIFY_CODE command
+ * See [1] 8.8
+ * This command challenges the card to prove integrity of COS binary code. For this purpose, the host application should have a special ‘hash library’ publicly provided
+ * by Tangem. It may contains ~150.000 precalculated hashes of COS binary code segments.
+ * VERIFY_CODE command internally reads a segment of COS binary code beginning at Code_Page_Address and having length of [64 x Code_Page_Count] bytes.
+ * Then it appends Challenge to the code segment, calculates resulting hash and returns it in the response.
+ * The application needs to ensure that returned hash coincides with the one stored in the hash library (see {@see Firmwares}).
+ *
+ * @param hashAlgID - ‘sha-256’, ‘sha-1’, ‘sha-224’, ‘sha-384’, ‘sha-512’, ‘crc-16’
+ * @param codePageAddress - Value from 0 to ~3000, take from {@see Firmwares}
+ * @param codePageCount - Number of 32-byte pages to read: from 1 to 5, take from {@see Firmwares}
+ * @param challenge - Additional challenge value from 1 to 10, take from {@see Firmwares}
+ * @return digest bytes to compare with one stored in {@see Firmwares}
+ * @throws Exception - if something went wrong
+ */
+ public byte[] run_VerifyCode(String hashAlgID, int codePageAddress, int codePageCount, byte[] challenge) throws Exception {
+ if (readResult == null) throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+ CommandApdu rqApdu = StartPrepareCommand(INS.VerifyCode);
+ rqApdu.addTLV(TLV.Tag.TAG_HashAlgID, hashAlgID.getBytes("US-ASCII"));
+ rqApdu.addTLV_U32(TLV.Tag.TAG_CodePageAddress, codePageAddress);
+ rqApdu.addTLV_U16(TLV.Tag.TAG_CodePageCount, codePageCount);
+ rqApdu.addTLV(TLV.Tag.TAG_Challenge, challenge);
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ return rspApdu.getTLVs().getTLV(TLV.Tag.TAG_CodeHash).Value;
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * VALIDATE_CARD command
+ * See [1] 3.2, 8.10
+ * This is an optional command that the issuer can support if there is a real risk of mass counterfeiting by making multiple clones of a single card. This can be the case for
+ * transferrable Tangem cards that are almost never redeemed by users.
+ * The issuer has to have a back-end service storing and updating a counter value (Card_Validation_Counter) for each card (CID). This function should also be supported
+ * by the issuer’s application.
+ * The application may occasionally call VALIDATE_CARD command to ensure that there’s only one card having this CID is circulating out there. VALIDATE_CARD
+ * will increase COS internal Card_Validation_Counter by 1 and sign the new value with CARD_PRIVATE_KEY. Then the application should submit increased
+ * Card_Validation_Counter and its signature to issuer’s card validation back-end (server). The server should verify the signature and update Card_Validation_Counter value if
+ * previous value is less than the new one. If the server reveals that submitted Card_Validation_Counter value is less than previous value, then the card having this CID is
+ * deemed compromised and should not be accepted by the application.
+ *
+ * @param PIN2 - PIN2 code to confirm operation
+ * @throws Exception - if something went wrong
+ */
+ private void run_ValidateCard(String PIN2) throws Exception {
+ if (readResult == null) throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+ CommandApdu rqApdu = StartPrepareCommand(INS.ValidateCard);
+ rqApdu.addTLV(TLV.Tag.TAG_PIN2, Util.calculateSHA256(PIN2));
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(true);
+ }
+ } else if (rspApdu.isStatus(SW_PIN_ERROR)) {
+ if (PIN2.equals(DefaultPIN2)) {
+ mCard.setUseDefaultPIN2(false);
+ }
+ throw new TangemException_InvalidPIN(String.format("FAILED: [%04X] - Possible the PIN or PIN2 is invalid!\n", rspApdu.getSW1SW2()));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * WRITE_ISSUER_DATA command
+ * This command re-writes Issuer_Data data block (max 512 bytes) and its issuer’s signature.
+ * Issuer_Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, format and payload of Issuer_Data.
+ * For example, this field may contain information about wallet balance signed by the issuer or additional issuer’s attestation data
+ *
+ * @param issuerData - new issuerData
+ * @param issuerSignature - signature of issuerData with IssuerDataKey
+ * @throws Exception - if something went wrong
+ */
+ public void run_WriteIssuerData(byte[] issuerData, byte[] issuerSignature) throws Exception {
+ if (readResult == null) throw new TangemException("Before run_VerifyCard execute run_Read card first!");
+
+ CommandApdu rqApdu = StartPrepareCommand(INS.WriteIssuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, issuerData);
+ rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerSignature);
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * GET_ISSUER_DATA command and verify verify issuer signature of returned data
+ * See [1] 3.3, 8.7
+ * This command returns Issuer_Data data block and its issuer’s signature.
+ *
+ * @return TLVList with issuerData (if success read and verify)
+ * @throws Exception - if something went wrong
+ */
+ public TLVList run_GetIssuerData() throws Exception {
+ CommandApdu rqApdu = StartPrepareCommand(INS.GetIssuerData);
+
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ TLV issuerData = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data);
+ TLV issuerDataSignature = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Signature);
+ TLV issuerDataCounter = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Counter);
+
+ boolean protectIssuerDataAgainstReplay = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
+
+ if (issuerData == null || issuerDataSignature == null)
+ throw new TangemException("Invalid answer format (GetIssuerData)");
+
+ ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream();
+ bsDataToVerify.write(mCard.getCID());
+ bsDataToVerify.write(issuerData.Value);
+ if (protectIssuerDataAgainstReplay) {
+ bsDataToVerify.write(issuerDataCounter.Value);
+ }
+ try {
+ if (CardCrypto.VerifySignature(mCard.getIssuerPublicDataKey(), bsDataToVerify.toByteArray(), issuerDataSignature.Value)) {
+ mCard.setIssuerData(issuerData.Value, issuerDataSignature.Value);
+ return TLVList.fromBytes(issuerData.Value);
+ } else {
+ throw new TangemException("Invalid issuer data read (signature verification failed)");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new TangemException("Invalid issuer data read");
+ }
+ } else {
+ throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
+ }
+ }
+
+ /**
+ * Execute consecutive READ commands with increasing encryption level from EncryptionMode.None to EncryptionMode.Strong, see {@link TangemCard.EncryptionMode}
+ * If card requires stricter encryption level it returns SW.NEED_ENCRYPTION status word
+ * Once READ is successfully executed - save answer to {@see readResult}, save current PIN and encryption mode to {@link TangemCard} and return
+ *
+ * @throws Exception - if something went wrong
+ */
+ public void run_GetSupportedEncryption() throws Exception {
+ mCard.encryptionMode = TangemCard.EncryptionMode.None;
+ do {
+ CommandApdu rqApdu = StartPrepareCommand(INS.Read);
+ Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
+
+ ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
+
+ if (rspApdu.isStatus(SW.NEED_ENCRYPTION)) {
+ if (mCard.encryptionMode == TangemCard.EncryptionMode.None) {
+ mCard.encryptionMode = TangemCard.EncryptionMode.Fast;
+ } else if (mCard.encryptionMode == TangemCard.EncryptionMode.Fast) {
+ mCard.encryptionMode = TangemCard.EncryptionMode.Strong;
+ } else {
+ throw new Exception("Can't get supported encryption methods");
+ }
+ } else if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
+ Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
+ readResult = rspApdu.getTLVs();
+ mCard.setPIN(mPIN);
+ break;
+ } else {
+ break;
+ }
+ } while (true);
+ }
+
+
+}
\ No newline at end of file
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/CommandApdu.java b/card-common/src/main/java/com/tangem/card_common/reader/CommandApdu.java
new file mode 100644
index 0000000000..9ecc6e9549
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/CommandApdu.java
@@ -0,0 +1,276 @@
+package com.tangem.card_common.reader;
+
+import com.tangem.card_common.util.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
+public class CommandApdu {
+
+ public static final byte ISO_CLA = (byte) 0x00;
+
+ protected String mCmdName;
+ protected int mCla = 0x00;
+ protected int mIns = 0x00;
+ protected int mP1 = 0x00;
+ protected int mP2 = 0x00;
+ protected int mLc = 0x00;
+
+ protected byte[] mData = new byte[0];
+
+ protected int mLe = 0x00;
+ protected boolean mLeUsed = false;
+ protected TLVList tlvList = new TLVList();
+
+ public CommandApdu() {
+ }
+
+ public CommandApdu(int cla, int ins, int p1, int p2) {
+ setCommandName(ins);
+ mCla = cla;
+ mIns = ins;
+ mP1 = p1;
+ mP2 = p2;
+ }
+
+ public CommandApdu(int cla, int ins, int p1, int p2, byte[] data) {
+ setCommandName(ins);
+ mCla = cla;
+ mIns = ins;
+ mLc = data.length;
+ mP1 = p1;
+ mP2 = p2;
+ mData = data;
+ }
+
+ public CommandApdu(INS ins) {
+ setCommandName(ins.name());
+ mCla = ISO_CLA;
+ mIns = ins.Code;
+ mP1 = 0;
+ mP2 = 0;
+ }
+
+ public CommandApdu(int cla, int ins, int p1, int p2, byte[] data, int le) {
+ setCommandName(ins);
+ mCla = cla;
+ mIns = ins;
+ mLc = data.length;
+ mP1 = p1;
+ mP2 = p2;
+ mData = data;
+ mLe = le;
+ mLeUsed = true;
+ }
+
+ public CommandApdu(int cla, int ins, int p1, int p2, int le) {
+ setCommandName(ins);
+ mCla = cla;
+ mIns = ins;
+ mP1 = p1;
+ mP2 = p2;
+ mLe = le;
+ mLeUsed = true;
+ }
+
+ public void setCommandName(String cmdName) {
+ mCmdName = cmdName;
+ }
+
+ private void setCommandName(int ins) {
+ INS ins1 = INS.ByCode(ins);
+ if (ins1 != null) {
+ mCmdName = ins1.toString();
+ } else {
+ mCmdName = String.format("INS[%2X]", ins);
+ }
+ }
+
+ public String getCommandName() {
+ return mCmdName;
+ }
+
+ public void setP1(int p1) {
+ mP1 = p1;
+ }
+
+ public void setP2(int p2) {
+ mP2 = p2;
+ }
+
+ public void setData(byte[] data) {
+ mLc = data.length;
+ mData = data;
+ }
+
+ public void addTLV(TLV.Tag tag, byte[] value) {
+ tlvList.add(new TLV(tag, value));
+ }
+
+ public void addTLV_U8(TLV.Tag tag, int U8) {
+ addTLV(tag, new byte[]{(byte) U8});
+ }
+
+ public void addTLV_U16(TLV.Tag tag, int U16) {
+ addTLV(tag, Util.intToByteArray2(U16));
+ }
+
+ public void addTLV_U32(TLV.Tag tag, int U32) {
+ addTLV(tag, Util.intToByteArray4(U32));
+ }
+
+ public void setLe(int le) {
+ mLe = le;
+ mLeUsed = true;
+ }
+
+ public int getP1() {
+ return mP1;
+ }
+
+ public int getP2() {
+ return mP2;
+ }
+
+ public int getLc() {
+ return mLc;
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+
+ public TLVList getTLVs() {
+ return tlvList;
+ }
+
+ public int getLe() {
+ return mLe;
+ }
+
+ public static String toString(byte[] cmdApdu, int Lc) {
+ String cmd = Util.bytesToHex(cmdApdu);
+ if (Lc == 0) return cmd;
+ return cmd.substring(0, 8) + " " + cmd.substring(8, 10) + " " +
+ cmd.substring(10, 10 + Lc * 2) + " " + cmd.substring(10 + Lc * 2, cmd.length());
+ }
+
+
+ public void Crypt(byte[] key) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException {
+ if (tlvList.size() != 0) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ for (TLV tlv : tlvList) {
+ try {
+ tlv.WriteToStream(stream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ break;
+ }
+ }
+
+ mData = stream.toByteArray();
+ byte[] crc = Util.calculateCRC16(mData);
+ stream = new ByteArrayOutputStream();
+ stream.write(Util.intToByteArray2(mData.length));
+ stream.write(crc);
+ stream.write(mData);
+ mData = stream.toByteArray();
+
+ byte[] mEncryptedData = CardCrypto.Encrypt(key, mData);
+
+ mData = mEncryptedData;
+ mLc = mData.length;
+
+ tlvList.clear();
+
+ }
+
+ }
+
+ public byte[] toBytes() {
+ int length = 4; // CLA, INS, P1, P2
+
+ if (tlvList.size() != 0) {
+ mData = tlvList.toBytes();
+ mLc = mData.length;
+ }
+
+ if (mData.length != 0) {
+ length += 1; // LC
+ if (mLc >= 256)
+ length += 2;
+ length += mData.length; // DATA
+ }
+ if (mLeUsed) {
+ length += 1; // LE
+ if (mLc >= 256)
+ length += 2;
+ }
+
+ byte[] apdu = new byte[length];
+
+ int index = 0;
+ apdu[index] = (byte) mCla;
+ index++;
+ apdu[index] = (byte) mIns;
+ index++;
+ apdu[index] = (byte) mP1;
+ index++;
+ apdu[index] = (byte) mP2;
+ index++;
+ if (mLc != 0) {
+ if (mLc < 256) {
+ apdu[index] = (byte) mLc;
+ index++;
+ } else {
+ apdu[index] = 0;
+ index++;
+ apdu[index] = (byte) (mLc >> 8);
+ index++;
+ apdu[index] = (byte) (mLc & 0xFF);
+ index++;
+ }
+
+ System.arraycopy(mData, 0, apdu, index, mData.length);
+ index += mData.length;
+ }
+ if (mLeUsed) {
+ if (mLc < 256) {
+ apdu[index] += (byte) mLe; // LE
+ } else {
+ apdu[index] = 0;
+ index++;
+ apdu[index] = (byte) (mLe >> 8);
+ index++;
+ apdu[index] = (byte) (mLe & 0xFF);
+ index++;
+ }
+ }
+
+ return apdu;
+ }
+
+ public CommandApdu clone() {
+ CommandApdu apdu = new CommandApdu();
+ apdu.setCommandName(mCmdName);
+ apdu.mCla = mCla;
+ apdu.mIns = mIns;
+ apdu.mP1 = mP1;
+ apdu.mP2 = mP2;
+ apdu.mLc = mLc;
+ apdu.mData = new byte[mData.length];
+ System.arraycopy(mData, 0, apdu.mData, 0, mData.length);
+ apdu.mLe = mLe;
+ apdu.mLeUsed = mLeUsed;
+ apdu.tlvList = new TLVList(tlvList);
+ return apdu;
+ }
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/INS.java b/card-common/src/main/java/com/tangem/card_common/reader/INS.java
new file mode 100644
index 0000000000..e3d56c3a06
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/INS.java
@@ -0,0 +1,35 @@
+package com.tangem.card_common.reader;
+
+/**
+ * Created by dvol on 07.03.2018.
+ */
+public enum INS {
+ Unknown(0x00),
+ Read(0xF2),
+ VerifyCard(0xF3),
+ ValidateCard(0xF4),
+ VerifyCode(0xF5),
+ WriteIssuerData(0xF6),
+ GetIssuerData(0xF7),
+ CreateWallet(0xF8),
+ CheckWallet(0xF9),
+ SwapPIN(0xFA),
+ Sign(0xFB),
+ PurgeWallet(0xFC),
+ Activate(0xFE),
+ OpenSession(0xFF);
+
+ INS(int Code) {
+ this.Code = Code;
+ }
+
+ public int Code;
+
+ public static INS ByCode(int Code) {
+ INS[] allINS = INS.values();
+ for (INS i : allINS) {
+ if (i.Code == Code) return i;
+ }
+ return Unknown;
+ }
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/NfcReader.java b/card-common/src/main/java/com/tangem/card_common/reader/NfcReader.java
new file mode 100644
index 0000000000..58b5e20693
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/NfcReader.java
@@ -0,0 +1,20 @@
+package com.tangem.card_common.reader;
+
+import java.io.IOException;
+
+public interface NfcReader {
+ byte[] getId();
+
+ void setTimeout(int timeout)throws IOException;
+
+ int getTimeout();
+
+ byte[] transceive(byte[] data) throws IOException;
+
+ void ignoreTag() throws IOException;
+
+ void notifyReadResult(boolean success);
+
+ void connect();
+
+}
\ No newline at end of file
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/ResponseApdu.java b/card-common/src/main/java/com/tangem/card_common/reader/ResponseApdu.java
new file mode 100644
index 0000000000..917dc79a50
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/ResponseApdu.java
@@ -0,0 +1,137 @@
+package com.tangem.card_common.reader;
+
+import com.tangem.card_common.util.Util;
+
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+
+public class ResponseApdu {
+
+ private int mSw1 = 0x00;
+ private int mSw2 = 0x00;
+
+ private byte[] mData = new byte[0];
+ private byte[] mBytes = new byte[0];
+
+ private TLVList tlvList = new TLVList();
+
+ private String parseError = null;
+
+ private ResponseApdu() {
+ }
+
+ ResponseApdu(byte[] respApdu) {
+ if (respApdu.length < 2) {
+ return;
+ }
+ if (respApdu.length > 2) {
+ mData = new byte[respApdu.length - 2];
+ System.arraycopy(respApdu, 0, mData, 0, respApdu.length - 2);
+
+ try {
+ tlvList = TLVList.fromBytes(mData);
+ } catch (TLVException e) {
+ parseError = e.getMessage();
+ }
+
+ }else{
+ tlvList=new TLVList();
+ parseError=null;
+ }
+ mSw1 = 0x00FF & respApdu[respApdu.length - 2];
+ mSw2 = 0x00FF & respApdu[respApdu.length - 1];
+ mBytes = respApdu;
+ }
+
+ public static boolean isStatusWord(byte[] respApdu, int SW)
+ {
+ int mSw1 = 0x00FF & respApdu[respApdu.length - 2];
+ int mSw2 = 0x00FF & respApdu[respApdu.length - 1];
+ return ((mSw1 << 8) | mSw2)==SW;
+ }
+
+ public static ResponseApdu Decrypt(byte[] data, byte[] key) throws Exception{
+
+ if( data.length==2 )
+ {
+ ResponseApdu responseApdu = new ResponseApdu();
+ responseApdu.mSw1 = ((int) data[0] & 0xFF);
+ responseApdu.mSw2 = ((int) data[1] & 0xFF);
+ return responseApdu;
+ }else if( data.length>=18 ){
+ byte[] decryptedData = CardCrypto.Decrypt(key, Arrays.copyOfRange(data, 0, data.length - 2),true);
+
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(decryptedData);
+ byte[] baLength = new byte[2];
+ inputStream.read(baLength);
+ int length = ((int) baLength[0] & 0xFF) * 256 + ((int) baLength[1] & 0xFF);
+ if (length > decryptedData.length - 4)
+ throw new Exception("Can't decrypt - data size invalid");
+ byte[] baCRC = new byte[2];
+ inputStream.read(baCRC);
+ byte[] answerData = new byte[length];
+ inputStream.read(answerData);
+ byte[] crc = Util.calculateCRC16(answerData);
+ if (!Arrays.equals(baCRC, crc)) throw new Exception("Can't decrypt - crc invalid");
+
+ ResponseApdu responseApdu = new ResponseApdu();
+ responseApdu.mSw1 = ((int) data[data.length - 2] & 0xFF);
+ responseApdu.mSw2 = ((int) data[data.length - 1] & 0xFF);
+ responseApdu.mBytes = data;
+ responseApdu.mData = answerData;
+
+ try {
+ responseApdu.tlvList = TLVList.fromBytes(answerData);
+ } catch (TLVException e) {
+ responseApdu.parseError = e.getMessage();
+ }
+
+ return responseApdu;
+ }else{
+ throw new Exception("Can't decrypt - data size to small");
+ }
+ }
+
+
+ public int getSW1() {
+ return mSw1;
+ }
+
+ public int getSW2() {
+ return mSw2;
+ }
+
+ public int getSW1SW2() {
+ return (mSw1 << 8) | mSw2;
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+
+ public TLVList getTLVs() {
+ return tlvList;
+ }
+
+ public boolean isParsedWithError() {
+ return parseError != null;
+ }
+ public String getParseErroMessage() {
+ return parseError;
+ }
+ public byte[] toBytes() {
+ return mBytes;
+ }
+
+ public boolean isStatus(int sw1sw2) {
+ if (getSW1SW2() == sw1sw2) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public String getSW1SW2Description() {
+ return SW.getDescription(getSW1SW2());
+ }
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/SW.java b/card-common/src/main/java/com/tangem/card_common/reader/SW.java
new file mode 100644
index 0000000000..3b09a7e819
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/SW.java
@@ -0,0 +1,43 @@
+package com.tangem.card_common.reader;
+
+/**
+ * Created by dvol on 07.03.2018.
+ */
+
+public class SW {
+ public static final int PROCESS_COMPLETED = 0x9000;
+ public static final int INVALID_PARAMS = 0x6A86;
+ public static final int ERROR_PROCESSING_COMMAND = 0x6286;
+ public static final int INVALID_STATE = 0x6985;
+ public static final int PINS_NOT_CHANGED = PROCESS_COMPLETED;
+ public static final int PIN1_CHANGED = PROCESS_COMPLETED + 0x0001;
+ public static final int PIN2_CHANGED = PROCESS_COMPLETED + 0x0002;
+ public static final int PINS_CHANGED = PROCESS_COMPLETED + 0x0003;
+ public static final int INS_NOT_SUPPORTED = 0x6D00;
+ public static final int NEED_ENCRYPTION = 0x6982;
+ public static final int NEED_PAUSE = 0x9789;
+
+ public static String getDescription(int sw) {
+ switch (sw) {
+ case ERROR_PROCESSING_COMMAND:
+ return "SW_ERROR_PROCESSING_COMMAND";
+ case INVALID_PARAMS:
+ return "SW_INVALID_PARAMS";
+ case INVALID_STATE:
+ return "SW_INVALID_STATE";
+ case INS_NOT_SUPPORTED:
+ return "SW_INS_NOT_SUPPORTED";
+ case NEED_ENCRYPTION:
+ return "SW_NEED_ENCRYPTION";
+ case PIN1_CHANGED:
+ return "SW_PIN1_CHANGED";
+ case PIN2_CHANGED:
+ return "SW_PIN2_CHANGED";
+ case PINS_CHANGED:
+ return "SW_PINS_CHANGED";
+ case PROCESS_COMPLETED:
+ return "SW_PROCESS_COMPLETED";
+ }
+ return "???";
+ }
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/SettingsMask.java b/card-common/src/main/java/com/tangem/card_common/reader/SettingsMask.java
new file mode 100644
index 0000000000..c073c45c7b
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/SettingsMask.java
@@ -0,0 +1,67 @@
+package com.tangem.card_common.reader;
+
+/**
+ * Created by dvol on 07.03.2018.
+ */
+
+public class SettingsMask {
+ public static final int IsReusable = 0x0001;
+ public static final int UseActivation = 0x0002;
+ public static final int ForbidPurgeWallet = 0x0004;
+ public static final int UseBlock = 0x0008;
+
+ public static final int AllowSwapPIN = 0x0010;
+ public static final int AllowSwapPIN2 = 0x0020;
+ public static final int UseCVC = 0x0040;
+ public static final int ForbidDefaultPIN = 0x0080;
+
+ public static final int UseOneCommandAtTime = 0x0100;
+ public static final int UseNDEF = 0x0200;
+ public static final int UseDynamicNDEF = 0x0400;
+ public static final int SmartSecurityDelay = 0x0800;
+
+ public static final int Protocol_AllowUnencrypted = 0x1000;
+ public static final int Protocol_AllowStaticEncryption = 0x2000;
+
+ public static final int ProtectIssuerDataAgainstReplay = 0x4000;
+
+ public static final int AllowSelectBlockchain = 0x8000;
+
+ public static final int DisablePrecomputedNDEF = 0x00010000;
+
+ public static String getDescription(int iValue) {
+ StringBuilder sb=new StringBuilder();
+ sb.append("[");
+ if ((iValue & SettingsMask.AllowSwapPIN) != 0) sb.append("AllowSwapPIN, ");
+ if ((iValue & SettingsMask.AllowSwapPIN2) != 0)
+ sb.append("AllowSwapPIN2, ");
+ if ((iValue & SettingsMask.ForbidDefaultPIN) != 0)
+ sb.append("ForbidDefaultPIN, ");
+ if ((iValue & SettingsMask.IsReusable) != 0) sb.append("IsReusable, ");
+ if ((iValue & SettingsMask.Protocol_AllowStaticEncryption) != 0)
+ sb.append("Protocol_AllowStaticEncryption, ");
+ if ((iValue & SettingsMask.Protocol_AllowUnencrypted) != 0)
+ sb.append("Protocol_AllowUnencrypted, ");
+ if ((iValue & SettingsMask.SmartSecurityDelay) != 0)
+ sb.append("SmartSecurityDelay, ");
+ if ((iValue & SettingsMask.UseActivation) != 0)
+ sb.append("UseActivation, ");
+ if ((iValue & SettingsMask.UseBlock) != 0) sb.append("UseBlock, ");
+ if ((iValue & SettingsMask.UseCVC) != 0) sb.append("UseCVC, ");
+ if ((iValue & SettingsMask.UseDynamicNDEF) != 0)
+ sb.append("UseDynamicNDEF, ");
+ if ((iValue & SettingsMask.UseNDEF) != 0) sb.append("UseNDEF, ");
+ if ((iValue & SettingsMask.UseOneCommandAtTime) != 0)
+ sb.append("UseOneCommandAtTime, ");
+ if ((iValue & SettingsMask.ProtectIssuerDataAgainstReplay) != 0)
+ sb.append("ProtectIssuerDataAgainstReplay, ");
+ if ((iValue & SettingsMask.ForbidPurgeWallet) != 0) sb.append("ForbidPurgeWallet, ");
+ if ((iValue & SettingsMask.AllowSelectBlockchain) != 0) sb.append("AllowSelectBlockchain, ");
+ if ((iValue & SettingsMask.DisablePrecomputedNDEF) != 0) sb.append("DisablePrecomputedNDEF, ");
+
+ if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length());
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/TLV.java b/card-common/src/main/java/com/tangem/card_common/reader/TLV.java
new file mode 100644
index 0000000000..f48d6d5906
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/TLV.java
@@ -0,0 +1,237 @@
+package com.tangem.card_common.reader;
+
+import com.tangem.card_common.util.Util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * Created by dvol on 23.06.2017.
+ */
+
+public class TLV {
+ public enum Tag {
+ TAG_Unknown(0x00),
+ TAG_CardID(0x01),
+ TAG_Status(0x02),
+ TAG_CardPublicKey(0x03),
+ TAG_CardSignature(0x04),
+ TAG_CurveID(0x05),
+ TAG_HashAlgID(0x06),
+ TAG_SigningMethod(0x07),
+ TAG_MaxSignatures(0x08),
+ TAG_PauseBeforePIN2(0x09),
+ TAG_SettingsMask(0x0A),
+ TAG_CardData(0x0C),
+ TAG_NDEFData(0x0D),
+ TAG_Health(0x0F),
+
+ TAG_PIN(0x10),
+ TAG_PIN2(0x11),
+ TAG_NewPIN(0x12),
+ TAG_NewPIN2(0x13),
+ TAG_NewPIN_Hash(0x14),
+ TAG_NewPIN2_Hash(0x15),
+ TAG_Challenge(0x16),
+ TAG_Salt(0x17),
+ TAG_ValidationCounter(0x18),
+ TAG_CVC(0x19),
+
+ TAG_Session_Key_A(0x1A),
+ TAG_Session_Key_B(0x1B),
+ TAG_Pause(0x1C),
+
+ TAG_Manufacture_ID(0x20),
+ TAG_Manufacturer_Signature(0x21),
+
+ TAG_Issuer_Data_PublicKey(0x30),
+ TAG_Issuer_Transaction_PublicKey(0x31),
+ TAG_Issuer_Data(0x32),
+ TAG_Issuer_Data_Signature(0x33),
+ TAG_Issuer_Transaction_Signature(0x34),
+ TAG_Issuer_Data_Counter(0x35),
+
+ TAG_IsActivated(0x3A),
+ TAG_ActivationSeed(0x3B),
+ TAG_ResetPIN(0x36),
+
+ TAG_CodePageAddress(0x40),
+ TAG_CodePageCount(0x41),
+ TAG_CodeHash(0x42),
+
+ TAG_TrOut_Hash(0x50),
+ TAG_TrOut_HashSize(0x51),
+ TAG_TrOut_Raw(0x52),
+
+ TAG_Wallet_PublicKey(0x60),
+ TAG_Signature(0x61),
+ TAG_RemainingSignatures(0x62),
+ TAG_SignedHashes(0x63),
+
+ TAG_Firmware(0x80),
+ TAG_Batch(0x81),
+ TAG_ManufactureDateTime(0x82),
+ TAG_Issuer_ID(0x83),
+ TAG_Blockchain_ID(0x84),
+ TAG_Manufacturer_PublicKey(0x85),
+ TAG_CardID_Manufacturer_Signature(0x86),
+
+ TAG_Token_Symbol(0xA0),
+ TAG_Token_Contract_Address(0xA1),
+ TAG_Token_Decimal(0xA2),
+ TAG_Denomination(0xC0),
+ TAG_ValidatedBalance(0xC1),
+ TAG_LastSign_Date(0xC2),
+ TAG_DenominationText(0xC3);
+
+
+ Tag(int Code) {
+ this.Code = Code;
+ }
+
+ public int getCode() {
+ return Code;
+ }
+
+ private int Code;
+
+ public static Tag ByCode(int Code) {
+ Tag[] allTags = Tag.values();
+ for (Tag t : allTags) if (t.getCode() == Code) return t;
+ return TAG_Unknown;
+ }
+ }
+
+ private Tag tag;
+
+ public Tag getTag() {
+ return tag;
+ }
+
+ public byte[] Value;
+
+ public TLV(Tag tag, byte[] value) {
+ this.tag = tag;
+ this.Value = value;
+ }
+
+ public void WriteToStream(ByteArrayOutputStream stream) throws IOException {
+ stream.write(tag.getCode());
+ if (Value != null) {
+ if (Value.length > 0xFE) {
+ stream.write(0xFF);
+ stream.write((Value.length >> 8) & 0xFF);
+ stream.write(Value.length & 0xFF);
+ } else {
+ stream.write(Value.length & 0xFF);
+ }
+ stream.write(Value);
+ } else {
+ stream.write(0x00);
+ }
+ }
+
+ public static TLV ReadFromStream(ByteArrayInputStream stream) throws IOException {
+ int code = stream.read();
+ if (code == -1) return null;
+ int len = stream.read();
+ if (len == -1)
+ throw new IOException("Can't read TLV");
+ if (len == 0xFF) {
+ int lenH = stream.read();
+ if (lenH == -1)
+ throw new IOException("Can't read TLV");
+ len = stream.read();
+ if (len == -1)
+ throw new IOException("Can't read TLV");
+ len |= (lenH << 8);
+ }
+ byte[] value = new byte[len];
+ if (len > 0) {
+ if (len != stream.read(value)) {
+ throw new IOException("Can't read TLV");
+ }
+ }
+ Tag tag = Tag.ByCode(code);
+ TLV result = new TLV(tag, value);
+ return result;
+ }
+
+ public int getAsInt() {
+ return Util.byteArrayToInt(Value);
+ }
+
+ public String getAsHexString() {
+ return Util.bytesToHex(Value);
+ }
+
+ public String getAsString() {
+ if( Value.length==0 ) return "";
+
+ if (Value[Value.length - 1] == 0) {
+ String s1 = new String(Arrays.copyOfRange(Value, 0, Value.length - 1), Charset.forName("utf-8"));
+ return s1.trim();
+ } else {
+ String s1 = new String(Value, Charset.forName("utf-8"));
+ return s1.trim();
+
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch (tag) {
+ case TAG_CardData:
+ case TAG_Issuer_Data: {
+ try {
+ TLVList tlvSub = TLVList.fromBytes(Value);
+ return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), tlvSub.toString());
+ } catch (TLVException e) {
+ e.printStackTrace();
+ }
+ if (Value != null) {
+ return String.format("%s[%d]: %s (non TLV)", tag.name(), Value.length, Util.bytesToHex(Value));
+ } else {
+ return String.format("%s[]: [[NULL]]", tag.name());
+ }
+ }
+ case TAG_CurveID:
+ case TAG_HashAlgID:
+ case TAG_Blockchain_ID:
+ case TAG_Manufacture_ID:
+ case TAG_Firmware:
+ case TAG_Issuer_ID:
+ case TAG_Token_Symbol:
+ if (Value != null) {
+ return String.format("%s[%d]: %s(%s)", tag.name(), Value.length, Util.bytesToHex(Value), getAsString());
+ } else {
+ return String.format("%s[]: [[NULL]]", tag.name());
+ }
+ case TAG_SettingsMask: {
+ StringBuilder sb=new StringBuilder();
+ if( Value!=null ) {
+ try {
+ int iValue = Util.byteArrayToInt(Value);
+ return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), SettingsMask.getDescription(iValue));
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
+ }
+ }else{
+ return String.format("%s[]: [[NULL]]", tag.name());
+ }
+ }
+ default:
+ if (Value != null) {
+ return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
+ } else {
+ return String.format("%s[]: [[NULL]]", tag.name());
+ }
+ }
+ }
+}
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/TLVException.java b/card-common/src/main/java/com/tangem/card_common/reader/TLVException.java
new file mode 100644
index 0000000000..07705f5ff3
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/TLVException.java
@@ -0,0 +1,18 @@
+package com.tangem.card_common.reader;
+
+public class TLVException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public TLVException(String message){
+ super(message);
+ }
+
+ public TLVException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TLVException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/card-common/src/main/java/com/tangem/card_common/reader/TLVList.java b/card-common/src/main/java/com/tangem/card_common/reader/TLVList.java
new file mode 100644
index 0000000000..a046696d01
--- /dev/null
+++ b/card-common/src/main/java/com/tangem/card_common/reader/TLVList.java
@@ -0,0 +1,72 @@
+package com.tangem.card_common.reader;
+
+/**
+ * Created by dvol on 23.06.2017.
+ */
+
+import com.tangem.card_common.util.Util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class TLVList extends ArrayList