You can download to install demo APK above to test with QPOS mpos directly
For more details, please visit our knowledge base : dspread.gitlab.io/qpos
- TOC {:toc}
Version | Author | Date | Description |
---|---|---|---|
0.1 | Austin Wang | 2016-05-01 | Initially Added |
1.0 | Austin Wang | 2016-09-01 | Added EMV related function |
1.1 | Ausitn Wang | 2017-03-01 | Merge QPOS standard and EMV Card reader together |
1.2 | Austin Wang | 2017-10-20 | Added UART interface support for GES device |
2.8.0 | Zhengwei Fang | 2018-05-14 | Change Maximum length of transaction amount to 12 |
2.9.0 | Zhengwei Fang | 2020-03-18 | Add CQPOSService to implement listener callback |
3.0.0 | Zhengwei Fang | 2020-06-04 | Update CVM pin and fix usb otg bug |
3.1.0 | Zhengwei Fang | 2020-07-27 | Add getRandomeNumByLen() and BLE ClearBluetooth function |
3.2.0 | Zhengwei Fang | 2020-12-17 | Add update TR31 keys,fix the "cashback is 0" issue |
3.3.0 | Zhengwei Fang | 2021-03-08 | Comment createInsecureRfcommSocketToServiceRecord function and add updateIPEKOperationByKeyType function |
3.4.0 | Zhengwei Fang | 2021-04-11 | Add doFelicaOp,generateTransportKey and updateIPEKByTransportKey function |
3.5.0 | Zhengwei Fang | 2021-05-08 | Add the sync sendapdu,sendNfcapdu function and multiple cards prompt for CR100, |
3.6.0 | Zhengwei Fang | 2021-06-15 | Add getPin function for CR100 & D20 and boardcastReceiver to detect the bluetooth if open or close |
3.7.0 | Zhengwei Fang | 2021-08-11 | Add clearD20Device,getD20SpLog function to pos log for D20&D30 |
3.8.0 | Zhengwei Fang | 2021-08-11 | Fix offline pin bug and change sdk android gradle veision to 30 |
3.9.0 | Zhengwei Fang | 2021-11-22 | Add modelInfo and change compile time format in the getQposInfo function |
4.0.0 | Zhengwei Fang | 2022-03-24 | Add the new method isBootMode to check the device boot status and update the multi-application selection for contactless |
4.1.0 | Zhengwei Fang | 2022-07-04 | Add playBuzzerByType(),operateLEDByType() function |
4.2.0 | Zhengwei Fang | 2022-08-25 | Add getEncryptedTrack2Data method to get ksn and EncryptedTrack2data. |
4.3.0 | Zhengwei Fang | 2023-03-02 | Fix the app crash bug caused by disconnecting Bluetooth during Bluetooth scanning |
QPOS is a serial of mobile payment devices. It can communicate with the mobile device through audio jack, UART or USB cable.
QPOS standard, QPOS mini, QPOS Plus, EMV06, EMV08, GEA and GES are all QPOS products, some of them are with PINPAD embedded and some of them are only card readers without PINPAD.
This document aims to help readers for using the Android SDK of QPOS.
Please access the below path to view the latest SDK version.
https://gitlab.com/dspread/android/-/packages
- Gradle Groovy DSL install command
implementation 'com.dspread.library:dspread_pos_sdk:4.4.5'
- Add Gradle Groovy DSL repository command
maven {
url 'https://gitlab.com/api/v4/projects/4128550/packages/maven'
}
All methods the SDK provided can be devided into three types:
- Init methods;
- Interactive methods;
- Listener methods.
The application use the init method to init the EMV card reader hardware and get an instance of the Card Reader. It then can use the interactive methods to start the communication with the card reader. During the communication process, if any message returned from the Card reader, a listener method will be invoked by the SDK package.
To avoid the application block and improve the speed of data interaction between the smart terminal and QPOS, the SDK framework is designed to work under asynchronous mode.
The Class named ‘QPOSService’ is the core of SDK library. Before the APP create this core instance with the parameter of “CommunicationMode mode”, the APP must register all the sub-functions in ‘QPOSServiceListener’. Below code snipplet shows how to init the SDK.
private void open(CommunicationMode mode) {
listener = new MyPosListener();
pos = QPOSService.getInstance(mode);
if (pos == null) {
statusEditText.setText("CommunicationMode unknow");
return;
}
pos.setConext(getApplicationContext());
Handler handler = new Handler(Looper.myLooper());
pos.initListener(handler, listener);
}
The CommunicaitonMode can be
public static enum CommunicationMode{
AUDIO,
BLUETOOTH_VER2,
UART,
USB
}
The app should choose appropriate communication mode depend on it's hardware configuration. Note, in the example above the app should realize the call back methods of MyPosListener.
The code below shows how to open the communication bridge with the open() method descripted above.
if (//we want to use Audio Jack as communication mode) {
open(CommunicationMode.AUDIO);
posType = POS_TYPE.AUDIO;
pos.openAudio();
} else if (//we want to use UART as communication mode) {
if (isUsb) {
open(CommunicationMode.USB);
posType = POS_TYPE.UART;
pos.openUsb();
}else {
open(CommunicationMode.UART);
posType = POS_TYPE.UART;
pos.openUart();
}
} else { //We will use Bluetooth
open(CommunicationMode.BLUETOOTH_VER2);
posType = POS_TYPE.BLUETOOTH;
//...
}
The app can get the EMV cardreader information by issuing:
pos.getQposInfo();
Note the pos is the instance of QPOSService, the app get it during the initialization process.
The device information will be returned on the below call back:
@Override
public void onQposInfoResult(Hashtable<String, String> posInfoData) {
String isSupportedTrack1 = posInfoData.get("isSupportedTrack1") == null ? ""
: posInfoData.get("isSupportedTrack1");
String isSupportedTrack2 = posInfoData.get("isSupportedTrack2") == null ? ""
: posInfoData.get("isSupportedTrack2");
String isSupportedTrack3 = posInfoData.get("isSupportedTrack3") == null ? ""
: posInfoData.get("isSupportedTrack3");
String bootloaderVersion = posInfoData.get("bootloaderVersion") == null ? ""
: posInfoData.get("bootloaderVersion");
String firmwareVersion = posInfoData.get("firmwareVersion") == null ? ""
: posInfoData.get("firmwareVersion");
String isUsbConnected = posInfoData.get("isUsbConnected") == null ? ""
: posInfoData.get("isUsbConnected");
String isCharging = posInfoData.get("isCharging") == null ? ""
: posInfoData.get("isCharging");
String batteryLevel = posInfoData.get("batteryLevel") == null ? ""
: posInfoData.get("batteryLevel");
String hardwareVersion = posInfoData.get("hardwareVersion") == null ? ""
: posInfoData.get("hardwareVersion");
}
App can knows the hardware , firmware version and hardware configuration based on the returned information.
The device ID is use to indentifying one paticular EMV card reader. The app use below method to get the device ID:
pos.getQposId();
The Device ID is returned to the app by below call back.
@Override
public void onQposIdResult(Hashtable<String, String> posIdTable) {
String posId = posIdTable.get("posId") == null ? "" : posIdTable
.get("posId");
}
The app can start a magnatic swipe card transaction, or an EMV chip card transaction, by below method:
pos.doTrade(60);
The only paramter is the time out value in second. If the user is using magnatic swipe card, after timeout seconds, the transaction will be timed out.
The transaction amount can be set by:
pos.setAmount(amount, cashbackAmount, "156",
TransactionType.GOODS);
the setAmount method can be called before start a transaction. If it was not called, a call back will be invoked by the SDK, giving app another chance to enter the transaction amount.
@Override
public void onRequestSetAmount() {
pos.setAmount(amount, cashbackAmount, "156",
TransactionType.GOODS);
}
The setAmount method has below parameters:
- amount : how much money in cents
- cashbackAmount : reserved for future use
- currency code : US Dollar, CNY, etc
- transactionType : which kind of transaction to be started. The transaction type can be:
public static enum TransactionType {
GOODS,
SERVICES,
CASH,
CASHBACK,
INQUIRY,
TRANSFER,
ADMIN,
CASHDEPOSIT,
PAYMENT
}
Transaction type is used mainly by the EMV Chip card transaction, for magnetic card, app can always use GOODS.
Magstripe card transaction is pretty simple. After the app start a transaction, if the user use a magnatic card, below callback will be called feeding the app magnatic card related information. The app then use the information returned for further processing.
@Override
public void onDoTradeResult(DoTradeResult result,
Hashtable<String, String> decodeData) {
if (result == DoTradeResult.NONE) {
statusEditText.setText(getString(R.string.no_card_detected));
} else if (result == DoTradeResult.ICC) {
statusEditText.setText(getString(R.string.icc_card_inserted));
TRACE.d("EMV ICC Start");
pos.doEmvApp(EmvOption.START);
} else if (result == DoTradeResult.NOT_ICC) {
statusEditText.setText(getString(R.string.card_inserted));
} else if (result == DoTradeResult.BAD_SWIPE) {
statusEditText.setText(getString(R.string.bad_swipe));
} else if (result == DoTradeResult.MCR) {
String maskedPAN = decodeData.get("maskedPAN");
String expiryDate = decodeData.get("expiryDate");
String cardHolderName = decodeData.get("cardholderName");
String ksn = decodeData.get("ksn");
String serviceCode = decodeData.get("serviceCode");
String track1Length = decodeData.get("track1Length");
String track2Length = decodeData.get("track2Length");
String track3Length = decodeData.get("track3Length");
String encTracks = decodeData.get("encTracks");
String encTrack1 = decodeData.get("encTrack1");
String encTrack2 = decodeData.get("encTrack2");
String encTrack3 = decodeData.get("encTrack3");
String partialTrack = decodeData.get("partialTrack");
String pinKsn = decodeData.get("pinKsn");
String trackksn = decodeData.get("trackksn");
String pinBlock = decodeData.get("pinBlock");
String encPAN = decodeData.get("encPAN");
String trackRandomNumber = decodeData
.get("trackRandomNumber");
String pinRandomNumber = decodeData.get("pinRandomNumber");
+ "\n";
}
} else if (result == DoTradeResult.NO_RESPONSE) {
statusEditText.setText(getString(R.string.card_no_response));
} else if (result == DoTradeResult.NO_UPDATE_WORK_KEY) {
statusEditText.setText("not update work key");
}
}
Below table describes the meaning of each data element SDK returned:
Key | Description |
---|---|
maskedPAN | Masked card number showing at most the first 6 and last 4 digits with in-between digits masked by “X” |
expiryDate | 4-digit in the form of YYMM in the track data |
cardHolderName | The cardholder name as seen on the card. This can be up to 26 characters. |
serviceCode | 3-digit service code in the track data |
track1Length | Length of Track 1 data |
track2Length | Length of Track 2 data |
track3Length | Length of Track 3 data |
encTracks | Reserved |
encTrack1 | Encrypted track 1 data with T-Des encryption key derived from DATA-key to be generated with trackksn and IPEK |
encTrack2 | Encrypted track 2 data with T-Des encryption key derived from DATA-key to be generated with trackksn and IPEK |
encTrack3 | Encrypted track 3 data with T-Des encryption key derived from DATA-key to be generated with trackksn and IPEK |
partialTrack | Reserved |
trackksn | KSN of the track data |
The track data returned in the hashtable is encrytped. It can be encrypted by Dukpt Data Key Variant 3DES CBC mode, or by Dukpt Data Key 3DES CBC mode. Per ANSI X9.24 2009 version request, The later (Data Key with 3DES CBC mode) is usually a recommanded choice.
Below is an example of the data captured during a live magnatic transaction, the track data is encrypted using data key variant, in 3DES CBC mode:
01-21 04:46:26.764: D/POS_SDK(30241): decodeData: {track3Length=0, track2Length=32, expiryDate=1011, encTrack3=, encPAN=, encTrack1=22FB2E931F3EFAFC8C3899AB779F3719E75D392365DB748EEA789560EEB7714D84AB7FFA5B2E162C9BD566D03DCD240FC9D316CAC4015B782294365F9062CA0A, pinRandomNumber=, encTrack2=153CEE49576C0B709515946D991CB48368FEA0375837ECA6, trackRandomNumber=, trackksn=00000332100300E00002, maskedPAN=622526XXXXXX5453, cardholderName=MR.ZHOU CHENG HAO , partialTrack=, encTracks=153CEE49576C0B709515946D991CB48368FEA0375837ECA6, psamNo=, formatID=30, track1Length=68, pinKsn=, serviceCode=106, ksn=, pinBlock=}
01-21 04:46:26.766: D/POS_SDK(30241): swipe card:Card Swiped:Format ID: 30
01-21 04:46:26.766: D/POS_SDK(30241): Masked PAN: 622526XXXXXX5453
01-21 04:46:26.766: D/POS_SDK(30241): Expiry Date: 1011
01-21 04:46:26.766: D/POS_SDK(30241): Cardholder Name: MR.ZHOU CHENG HAO
01-21 04:46:26.766: D/POS_SDK(30241): trackksn: 00000332100300E00002
01-21 04:46:26.766: D/POS_SDK(30241): Service Code: 106
01-21 04:46:26.766: D/POS_SDK(30241): Track 1 Length: 68
01-21 04:46:26.766: D/POS_SDK(30241): Track 2 Length: 32
01-21 04:46:26.766: D/POS_SDK(30241): Track 3 Length: 0
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Tracks: 153CEE49576C0B709515946D991CB48368FEA0375837ECA6
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 1: 22FB2E931F3EFAFC8C3899AB779F3719E75D392365DB748EEA789560EEB7714D84AB7FFA5B2E162C9BD566D03DCD240FC9D316CAC4015B782294365F9062CA0A
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 2: 153CEE49576C0B709515946D991CB48368FEA0375837ECA6
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 3:
01-21 04:46:26.766: D/POS_SDK(30241): pinKsn: 00000332100300E000C6
01-21 04:46:26.766: D/POS_SDK(30241): pinBlock: 377D28B8C7EF080A
01-21 04:46:26.766: D/POS_SDK(30241): encPAN:
01-21 04:46:26.766: D/POS_SDK(30241): trackRandomNumber:
01-21 04:46:26.766: D/POS_SDK(30241): pinRandomNumber:
The track ksn 00000332100300E00002 can be used to decode the track data:
Track 1 data: 22FB2E931F3EFAFC8C3899AB779F3719E75D392365DB748EEA789560EEB7714D84AB7FFA5B2E162C9BD566D03DCD240FC9D316CAC4015B782294365F9062CA0A
Track 2 data: 153CEE49576C0B709515946D991CB48368FEA0375837ECA6
Below python script demostrate how to decode track data encrypted with DataKey Variant in CBC mode:
def GetDataKeyVariant(ksn, ipek):
key = GetDUKPTKey(ksn, ipek)
key = bytearray(key)
key[5] ^= 0xFF
key[13] ^= 0xFF
return str(key)
def TDES_Dec(data, key):
t = triple_des(key, CBC, padmode=None)
res = t.decrypt(data)
return res
def decrypt_card_info(ksn, data):
BDK = unhexlify("0123456789ABCDEFFEDCBA9876543210")
ksn = unhexlify(ksn)
data = unhexlify(data)
IPEK = GenerateIPEK(ksn, BDK)
DATA_KEY_VAR = GetDataKeyVariant(ksn, IPEK)
print hexlify(DATA_KEY_VAR)
res = TDES_Dec(data, DATA_KEY_VAR)
return hexlify(res)
Using data key variant to decrypt track 1, will get:
16259249 54964104 16598554 553FADC8 EEA8BF50 23A25BA7 02886F00 00000000 0003E450 45145059 15D44964 10653590 41041041 F0000000 00000000 00000000
Each character in Track 1 is 6 bits in length, 4 characters are packed into 3 bytes. Each character is mapped from 0x20 to 0x5F. So to get the real ASCII value of each charactor, you need to add 0x20 to each decoded 6 bits.
For example, the leading 3 bytes of above track 1 data is 16,25,92
Which in binary is: 00010110 00100101 10010010
Unpacked them to 4 bytes: 000101 100010 010110 010010
Which in binary is:05221612
Add 0x20 to each byte:25423632
Which is in ASCII :%B62
Using data key variant to decrypt track 2, will get:
62252600 06685453 D1011106 17426936 FFFFFFFF FFFFFFFF
Each character in Track 2 & Track 3 is 4 bits in length. 2 characters are packed into 1 byte and padded with zero before encryption
Below is another example, the track data is encrypted using data key whith 3DES CBC mode (per ANSI X9.24 2009 version request)
01-21 04:46:26.764: D/POS_SDK(30241): decodeData: {track3Length=0, track2Length=32, expiryDate=1011, encTrack3=, encPAN=, encTrack1=22FB2E931F3EFAFC8C3899AB779F3719E75D392365DB748EEA789560EEB7714D84AB7FFA5B2E162C9BD566D03DCD240FC9D316CAC4015B782294365F9062CA0A, pinRandomNumber=, encTrack2=153CEE49576C0B709515946D991CB48368FEA0375837ECA6, trackRandomNumber=, trackksn=00000332100300E00002, maskedPAN=622526XXXXXX5453, cardholderName=MR.ZHOU CHENG HAO , partialTrack=, encTracks=153CEE49576C0B709515946D991CB48368FEA0375837ECA6, psamNo=, formatID=30, track1Length=68, pinKsn=, serviceCode=106, ksn=, pinBlock=}
01-21 04:46:26.766: D/POS_SDK(30241): swipe card:Card Swiped:Format ID: 30
01-21 04:46:26.766: D/POS_SDK(30241): Masked PAN: 622526XXXXXX5453
01-21 04:46:26.766: D/POS_SDK(30241): Expiry Date: 1011
01-21 04:46:26.766: D/POS_SDK(30241): Cardholder Name: MR.ZHOU CHENG HAO
01-21 04:46:26.766: D/POS_SDK(30241): KSN:
01-21 04:46:26.766: D/POS_SDK(30241): pinKsn:
01-21 04:46:26.766: D/POS_SDK(30241): trackksn: 00000332100300E00002
01-21 04:46:26.766: D/POS_SDK(30241): Service Code: 106
01-21 04:46:26.766: D/POS_SDK(30241): Track 1 Length: 68
01-21 04:46:26.766: D/POS_SDK(30241): Track 2 Length: 32
01-21 04:46:26.766: D/POS_SDK(30241): Track 3 Length: 0
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Tracks: 153CEE49576C0B709515946D991CB48368FEA0375837ECA6
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 1: 22FB2E931F3EFAFC8C3899AB779F3719E75D392365DB748EEA789560EEB7714D84AB7FFA5B2E162C9BD566D03DCD240FC9D316CAC4015B782294365F9062CA0A
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 2: 153CEE49576C0B709515946D991CB48368FEA0375837ECA6
01-21 04:46:26.766: D/POS_SDK(30241): Encrypted Track 3:
01-21 04:46:26.766: D/POS_SDK(30241): Partial Track:
01-21 04:46:26.766: D/POS_SDK(30241): pinBlock:
01-21 04:46:26.766: D/POS_SDK(30241): encPAN:
01-21 04:46:26.766: D/POS_SDK(30241): trackRandomNumber:
01-21 04:46:26.766: D/POS_SDK(30241): pinRandomNumber:
Below python script demostrate how to decode track data encrypted with DataKey in CBC mode:
def GetDataKeyVariant(ksn, ipek):
key = GetDUKPTKey(ksn, ipek)
key = bytearray(key)
key[5] ^= 0xFF
key[13] ^= 0xFF
return str(key)
def GetDataKey(ksn, ipek):
key = GetDataKeyVariant(ksn, ipek)
return str(TDES_Enc(key,key))
def TDES_Dec(data, key):
t = triple_des(key, CBC, "\0\0\0\0\0\0\0\0",padmode=None)
res = t.decrypt(data)
return res
def decrypt_card_info(ksn, data):
BDK = unhexlify("0123456789ABCDEFFEDCBA9876543210")
ksn = unhexlify(ksn)
data = unhexlify(data)
IPEK = GenerateIPEK(ksn, BDK)
DATA_KEY = GetDataKey(ksn, IPEK)
print hexlify(DATA_KEY)
res = TDES_Dec(data, DATA_KEY)
return hexlify(res)
The decoded track 1 and track 2 data are the same as the track data we got in previous section.
The QPOS will also send the encryted PIN to the mobile application:
10-07 11:37:49.571: V/vahid(20753): ???? ????? ??:Format ID: 30
10-07 11:37:49.571: V/vahid(20753): Masked PAN: 622622XXXXXX3256
10-07 11:37:49.571: V/vahid(20753): Expiry Date: 2612
10-07 11:37:49.571: V/vahid(20753): Cardholder Name:
10-07 11:37:49.571: V/vahid(20753): KSN:
10-07 11:37:49.571: V/vahid(20753): pinKsn: 09118041200085E0000B
10-07 11:37:49.571: V/vahid(20753): trackksn: 09118041200085E00013
10-07 11:37:49.571: V/vahid(20753): Service Code: 220
10-07 11:37:49.571: V/vahid(20753): Track 1 Length: 0
10-07 11:37:49.571: V/vahid(20753): Track 2 Length: 37
10-07 11:37:49.571: V/vahid(20753): Track 3 Length: 0
10-07 11:37:49.571: V/vahid(20753): Encrypted Tracks: 1909568B7256B930EC0DFAB30061B640F24CD3CD0006D349
10-07 11:37:49.571: V/vahid(20753): Encrypted Track 1:
10-07 11:37:49.571: V/vahid(20753): Encrypted Track 2: 1909568B7256B930EC0DFAB30061B640F24CD3CD0006D349
10-07 11:37:49.571: V/vahid(20753): Encrypted Track 3:
10-07 11:37:49.571: V/vahid(20753): Partial Track:
10-07 11:37:49.571: V/vahid(20753): pinBlock: FFB0DFF5141385FA
10-07 11:37:49.571: V/vahid(20753): encPAN:
10-07 11:37:49.571: V/vahid(20753): trackRandomNumber:
10-07 11:37:49.571: V/vahid(20753): pinRandomNumber:
Decode the Track 2 data using the method descripted before: 6226220129263256D26122200059362100000FFFFFFFFFFF
Below python script demostrate how to decode PINBLOCK:
def GetPINKeyVariant(ksn, ipek):
key = GetDUKPTKey(ksn, ipek)
key = bytearray(key)
key[7] ^= 0xFF
key[15] ^= 0xFF
return str(key)
def TDES_Dec(data, key):
t = triple_des(key, CBC, padmode=None)
res = t.decrypt(data)
return res
def decrypt_pinblock(ksn, data):
BDK = unhexlify("0123456789ABCDEFFEDCBA9876543210")
ksn = unhexlify(ksn)
data = unhexlify(data)
IPEK = GenerateIPEK(ksn, BDK)
PIN_KEY = GetPINKeyVariant(ksn, IPEK)
print hexlify(PIN_KEY)
res = TDES_Dec(data, PIN_KEY)
return hexlify(res)
if __name__ == "__main__":
KSN = "09118041200085E0000B"
DATA = "FFB0DFF5141385FA"
#DATA="1909568B7256B930EC0DFAB30061B640F24CD3CD0006D349"
print decrypt_pinblock(KSN, DATA)
The decrypted PINBLOCK (formated Pin data) is: 041173DFED6D9CDA The real PIN value can be caculated using formated pin data and PAN as inputs, according to ANSI X9.8. Below is an example:
- PAN: 6226220129263256
- 12 right most PAN digits without checksum: 622012926325
- Add 0000 to the left: 0000622012926325
- XOR (#3) and Formated PIN Data
XOR (0000622012926325, 041173DFED6D9CDA) = 041111FFFFFFFFFF In our example, the plain PIN is 4 bytes in length with data "1111"
EMV Chip card transaction is much more complicate than magnatic swipe card transaction. The EMV kernel inside the device may need a lot of information to process the transaction, including:
- PIN from the card holder
- Current time from the application
- Preferred EMV application from card holder
- The process result from the bank (card issuer) for the transaction
The app start the EMV transaction by calling
pos.doEmvApp(EmvOption.START);
This is usually happens inside the call back of onDoTradeResult(), as below demo code shows:
@Override
public void onDoTradeResult(DoTradeResult result,
Hashtable<String, String> decodeData) {
if (result == DoTradeResult.NONE) {
statusEditText.setText(getString(R.string.no_card_detected));
} else if (result == DoTradeResult.ICC) {
statusEditText.setText(getString(R.string.icc_card_inserted));
TRACE.d("EMV ICC Start")
pos.doEmvApp(EmvOption.START);
} else if (result == DoTradeResult.NOT_ICC) {
statusEditText.setText(getString(R.string.card_inserted));
} else if (result == DoTradeResult.BAD_SWIPE) {
statusEditText.setText(getString(R.string.bad_swipe));
} else if (result == DoTradeResult.MCR) {
//handling MSR transaction
}
The PIN information can be sent to the EMV kernel by:
@Override
public void onRequestSetPin() {
pos.sendPin("123456");
//pos.emptyPin(); //Bypass PIN Entry
//pos.cancelPin(); //Cancel the transaction
}
Note, the kernel will not call the callback if PIN is not required for the transaction, or if the QPOS itself is with an embedded PINPAD.
If the user do not want to input PIN, the applicaiton can bypass PIN enter by calling
pos.emptyPin();
if the user want to cancel the transaction, the app should call
pos.cancelPin();
The current time information can be sent to the EMV kernel by:
@Override
public void onRequestTime() {
String terminalTime = new SimpleDateFormat("yyyyMMddHHmmss")
.format(Calendar.getInstance().getTime());
pos.sendTime(terminalTime);
}
If there is multiple EMV applications inside one Chip card, the SDK will ask the user to choose one application from a list:
@Override
public void onRequestSelectEmvApp(ArrayList<String> appList) {
pos.selectEmvApp(position); //position is the index of the chosen application
//pos.cancelSelectEmvApp(); //Cancel the transaction
}
The chosen application is sending to the EMV kernel by
pos.selectEmvApp(position)
If the EMV kernel found the transaction need to go online, below call back will be called.
@Override
public void onRequestOnlineProcess(String tlv) {
//sending online message tlv data to issuer
....
//send the received online processing result to POS
pos.sendOnlineProcessResult("8A023030");
}
Below is an exmple of tlv data received by onRequestOnlineProcess:
2014-08-27 17:52:21.210 qpos-ios-demo[391:60b] alertView.title = Online process requested.
2014-08-27 17:52:21.211 qpos-ios-demo[391:60b] hideAlertView
2014-08-27 17:52:21.221 qpos-ios-demo[391:60b] onRequestOnlineProcess = {
tlv = 5F200220204F08A0000003330101015F24032312319F160F4243544553542031323334353637389F21031752139A031408279F02060000000000019F03060000000000009F34034203009F120A50424F43204445424954C409623061FFFFFFFF5284C10A00000332100300E00003C708A68701E68CB34BDEC00A00000332100300E00003C2820150E84B5D0D2AA9F40A2EFCC52424C52DDE2ABB1A07F8B53A8F37837A9AA4BF7200CC55AA1480ED5665AEC03DFE493248AEEA126345F1C2BA0EB0AA82546CC0AF5E6F4E40D7F9A3788C8F35B33F5AF1D85231D77FCE112A1C9D2AFF3679C3C46456232D32FD0D2AAF288CFD4CC52C1F33F128C247296C9E46647D930ACED5B34CFD0C2A823B3F91BEC60E8280005CB96C3EFCCC352F0A30F77A2A033361B5C2C720D8B6E85BFA3C589ADBD6FAF15D3C520085A5276B736860441BB15DBF8FA537708654EE90E32C194D1487362498F59346706FD797DFC8DD28FCF31E7D49886BA62779EC42411A54F03FE22B9431969B780E8280005CB96C3EEF460C1F76C0F2217EAC9B999E3E03128A93A11A4FC6885E4106A4EA4D815D10900AC6AC95E3325D585CB8678AE17A4DEE4C45E2E44209B9493B5FD94F3F46CCF730CD8FED9430B7574CE670018A94907B2AA4B475A93ABF;
}
The tlv data can be decoded using the online EMVlab tool:
As we can see from the decoded table:
Tag | Tag Name | Value |
---|---|---|
5F20 | Cardholder Name | |
4F | AID | A000000333010101 |
5F24 | App Expiration Date | 231231 |
9F16 | Merchant ID | B C T E S T 1 2 3 4 5 6 7 8 |
9F21 | Transaction Time | 175213 |
... | ... | ... |
C4 | Masked PAN | 623061FFFFFFFF5284 |
C1 | KSN(PIN) | 00000332100300E00003 |
C7 | PINBLOCK | A68701E68CB34BDE |
C0 | KSN Online Msg | 00000332100300E00003 |
C2 | Online Message | E84B5D0D2AA9F40A2EFC.... |
Inside the table, there are:
- Some EMV TAGs (5F20,4F,5F24 ...) with plain text value.
- Some Proprietary tags starting with 0xC, in our case C4,C1,C7,C0 and C2.
The defination of proprietary tags can be found below:
Tag | Name | Length(Bytes) |
---|---|---|
C0 | KSN of Online Msg | 10 |
C1 | KSN of PIN | 10 |
C2 | Online Message(E) | var |
C3 | KSN of Batch/Reversal Data | 10 |
C4 | Masked PAN | 0~10 |
C5 | Batch Data | var |
C6 | Reversal Data | var |
C7 | PINBLOCK | 8 |
It's the responsibility of the app to handle the online message string, sending them to the bank( the cardd issuer), and check the bank processing result.
The value of tag C2 is the encrypted Online Message, usually the app need to send it to the back end system, along with the tag C0 value. The backend system can derive the 3DES key from C0 value, and decrypt the C2 value and get the real online data in plain text format.
In case encrypted PIN is needed by the transaction, the app can also send the value of tag C7,C1 to back end system.
The example above is just a demostration. "8A023030" is a fake result from back end system.
As an exmple of decoding the online message, please find below some demo scripts:
def decrypt_icc_info(ksn, data):
BDK = unhexlify("0123456789ABCDEFFEDCBA9876543210")
ksn = unhexlify(ksn)
data = unhexlify(data)
IPEK = GenerateIPEK(ksn, BDK)
DATA_KEY = GetDataKey(ksn, IPEK)
print hexlify(DATA_KEY)
res = TDES_Dec(data, DATA_KEY)
return hexlify(res)
if __name__ == "__main__":
KSN = "00000332100300E00003"
DATA = "E84B5D0D2AA9F40A2EFCC52424C52DDE2ABB1A07F8B53A8F37837A9AA4BF7200CC55AA1480ED5665AEC03DFE493248AEEA126345F1C2BA0EB0AA82546CC0AF5E6F4E40D7F9A3788C8F35B33F5AF1D85231D77FCE112A1C9D2AFF3679C3C46456232D32FD0D2AAF288CFD4CC52C1F33F128C247296C9E46647D930ACED5B34CFD0C2A823B3F91BEC60E8280005CB96C3EFCCC352F0A30F77A2A033361B5C2C720D8B6E85BFA3C589ADBD6FAF15D3C520085A5276B736860441BB15DBF8FA537708654EE90E32C194D1487362498F59346706FD797DFC8DD28FCF31E7D49886BA62779EC42411A54F03FE22B9431969B780E8280005CB96C3EEF460C1F76C0F2217EAC9B999E3E03128A93A11A4FC6885E4106A4EA4D815D10900AC6AC95E3325D585CB8678AE17A4DEE4C45E2E44209B9493B5FD94F3F46CCF730CD8FED9430B7574CE670018A94907B2AA4B475A93ABF"
print decrypt_icc_info(KSN, DATA)
The decoded icc online message looks like:
All the online message in embedded inside tag 0x70, the ending 00 are paddings for 3DES encryption.
The application will be notified by the SDK regarding the transaction result by:
@Override
public void onRequestTransactionResult(
TransactionResult transactionResult) {
if (transactionResult == TransactionResult.APPROVED) {
} else if (transactionResult == TransactionResult.TERMINATED) {
} else if (transactionResult == TransactionResult.DECLINED) {
} else if (transactionResult == TransactionResult.CANCEL) {
} else if (transactionResult == TransactionResult.CAPK_FAIL) {
} else if (transactionResult == TransactionResult.NOT_ICC) {
} else if (transactionResult == TransactionResult.SELECT_APP_FAIL) {
} else if (transactionResult == TransactionResult.DEVICE_ERROR) {
} else if (transactionResult == TransactionResult.CARD_NOT_SUPPORTED) {
} else if (transactionResult == TransactionResult.MISSING_MANDATORY_DATA) {
} else if (transactionResult == TransactionResult.CARD_BLOCKED_OR_NO_EMV_APPS) {
} else if (transactionResult == TransactionResult.INVALID_ICC_DATA) {
}
}
}
When the transaction is finished. The batch data will be returned to the application by below callback.
@Override
public void onRequestBatchData(String tlv) {
}
Note, if there is issuer's script result inside the tlv, the mobile app need to feedback it to the bank. Decoding the tlv inside onRequestBatchData is similar to decoding onRequestOnlineProcess.
If the EMV chip card refuse the transaction, but the transaction was approved by the issuer. A reversal procedure should be initiated by the mobile app. The requred data for doing reversal can be got by below call back:
@Override
public void onReturnReversalData(String tlv) {
...
}
During the transaction, if there is anything abnormal happened, the onError callback will be called.
@Override
public void onError(Error errorState) {
if (errorState == Error.CMD_NOT_AVAILABLE) {
} else if (errorState == Error.TIMEOUT) {
} else if (errorState == Error.DEVICE_RESET) {
} else if (errorState == Error.UNKNOWN) {
} else if (errorState == Error.DEVICE_BUSY) {
} else if (errorState == Error.INPUT_OUT_OF_RANGE) {
} else if (errorState == Error.INPUT_INVALID_FORMAT) {
} else if (errorState == Error.INPUT_ZERO_VALUES) {
} else if (errorState == Error.INPUT_INVALID) {
} else if (errorState == Error.CASHBACK_NOT_SUPPORTED) {
} else if (errorState == Error.CRC_ERROR) {
} else if (errorState == Error.COMM_ERROR) {
} else if (errorState == Error.MAC_ERROR) {
} else if (errorState == Error.CMD_TIMEOUT) {
}
}