Skip to main content

Embed Code

The action VOID is applicable for transactions before settlement or on the same day.
The action REFUND is applicable for transactions after settlement or beyond one day.
DuitNow QR payments are eligible for REFUND only.

The source code is written in Java for native Android development.


Step 1 : Implement ECC Libraries

Implement ECC Libraries in build.gradle from .../android/app.

android {
...
defaultConfig {
...
multiDexEnabled true
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/NOTICE.md'
}
...
}

dependencies {
...
// Crypto
implementation 'com.nimbusds:nimbus-jose-jwt:8.9'
implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
...
}

Step 2 : Private and Public Keys

Generate both the public and private keys.

Step 2.1 : Generate Keys

Please refer to Appendix 4 to generate EC Keypair.


Step 2.2 : Initialize Keys

Initialize the keys that will be used for the encryption and decryption process.

TestPos(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
try {
thirdAppPrivateKey = loadECPrivateKey(reactContext.getAssets().open("ec_private_key_pkcs8.pem"));
thirdAppPublicKey = loadECPublicKey(reactContext.getAssets().open("ec_public_key.pem"));
yippiePosPublicKey = loadECPublicKey(reactContext.getAssets().open("yippiepos_public_key.key"));
String refNum = "1.0";
secretKey = generateECDHSecret(thirdAppPrivateKey, yippiePosPublicKey, refNum);
} catch (IOException ex) {
ex.printStackTrace();
}

BA = BluetoothAdapter.getDefaultAdapter();
}

Step 3 : Load the Initialized Keys

Load the initialized keys from the key file.

For Private Key
public static ECPrivateKey loadECPrivateKey(InputStream certFile) {
ECPrivateKey ecPrivateKey = null;
PrivateKey privateKey = null;
try {
byte[] encodedPrivateKey = new byte[certFile.available()];
certFile.read(encodedPrivateKey);
certFile.close();
try {
String certFileContent = new String(encodedPrivateKey)
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----","")
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "");
encodedPrivateKey = android.util.Base64.decode(certFileContent, android.util.Base64.DEFAULT);
} catch (Exception ex) {
ex.printStackTrace();
}
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
privateKey = keyFactory.generatePrivate(privateKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
e.printStackTrace();
}
if (privateKey instanceof ECPrivateKey){
ecPrivateKey = (ECPrivateKey) privateKey;
}
return ecPrivateKey;
}

Step 4 : Declare Input

Declare all input parameters in the request and create a JSON string.

Note
  • Void applicable for transactions before settlement or within the same day.
  • Refund applicable for transactions after settlement or beyond one day.
  • DuitNow QR payments are eligible for Refund only.
Void Request Parameters
public void receiveMessage(ReadableMap data) {
String msgVer = data.getString("msgVer");
String callerDevice = data.getString("callerDevice");
String callerDeviceVer = data.getString("callerDeviceVer");
String txnID = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
String txnDateTime = data.getString("txnDateTime");
String email = data.getString("email");
String password = data.getString("password");
String crcyCde = data.getString("crcyCde");
String amt = data.getString("amt");
String stan = data.getString("stan");
String authIdResp = data.getString("authIdResp");
String rrn = data.getString("rrn");
String mrn = data.getString("mrn");

String jsonMessage = "{'TxnTyp' : '" + TestPos.TransactionType.VOD +
"', 'MsgVer' : '" + msgVer +
"', 'CallerDeviceType' : '" + callerDevice +
"', 'CallerDeviceVer' : '" + callerDeviceVer +
"', 'TxnID' : '" + txnID +
"', 'LocalTxnDTTime' : '" + txnDateTime +
"', 'Email' : '" + email +
"', 'Pwd' : '" + password +
"', 'AmtTxn' : '" + amt +
"', 'CrcyTxn' : '" + crcyCde +
"', 'STAN' : '" + stan +
"', 'AuthIdResp' : '" + authIdResp +
"', 'RRN' : '" + rrn +
"', 'MRN' : '" + mrn + "'}";

ECKey publicKeyASJWK = new ECKey.Builder(Curve.P_256, thirdAppPublicKey)
.keyID(UUID.randomUUID().toString())
.build();

String jsonMessageEncrypted = encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage);

startActivity(REQ_VOID, jsonMessageEncrypted);
}

Step 5 : Encrypt String

After obtaining the jsonMessage as a string, encrypt it using the encryptWithFormatJWE function.

public static String encryptWithFormatJWE(String transactionID, String sdkPublicKeyAsJWK, SecretKey secretKey, JWEAlgorithm jweAlgorithm, String message) {
String encryptedMessage = "";

try {
//header
JWEHeader.Builder builder = new JWEHeader.Builder(jweAlgorithm, EncryptionMethod.A128CBC_HS256);
if (!TextUtils.isEmpty(transactionID)) {
builder.keyID(transactionID);
}
if (!TextUtils.isEmpty(sdkPublicKeyAsJWK)) {
builder.ephemeralPublicKey(ECKey.parse(sdkPublicKeyAsJWK));
}
JWEHeader header = builder.build();

//encrypt
if(jweAlgorithm == JWEAlgorithm.DIR) {
Provider bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance();
Security.addProvider((bouncyCastleProvider));
JWEObject jweObject = new JWEObject(header, new Payload(message));
jweObject.encrypt(new DirectEncrypter(secretKey));
encryptedMessage = jweObject.serialize();
} else if (jweAlgorithm == JWEAlgorithm.ECDH_ES) {
// build JWE structure
JWECryptoParts jweParts = ContentCryptoProvider.encrypt(header, message.getBytes(), secretKey, null, new JWEJCAContext());

// JWE Protected Header
StringBuilder stringBuilder = new StringBuilder(header.toBase64URL().toString());
stringBuilder.append('.');

// Encrypted Key
if (jweParts.getEncryptedKey() != null) {
stringBuilder.append(jweParts.getEncryptedKey().toString());
}
stringBuilder.append('.');

// Initialization Vector
if (jweParts.getInitializationVector() != null) {
stringBuilder.append(jweParts.getInitializationVector().toString());
}
stringBuilder.append('.');

// Ciphertext
stringBuilder.append(jweParts.getCipherText().toString());
stringBuilder.append('.');

// Authentication Tag
if (jweParts.getAuthenticationTag() != null) {
stringBuilder.append(jweParts.getAuthenticationTag().toString());
}
encryptedMessage = stringBuilder.toString();
} else {
Log.d("Tag", "Unsupported other algorithms");
}
} catch (Exception ex) {
ex.printStackTrace();
}
return encryptedMessage;
}

Step 6 : Perform App-to-app calling using INTENT

To use INTENT, both the Kayaaku POS app and the merchant's POS app must be installed on the same device.

Use startActivityForResult() as per the sample code below.

INTENT
// start intent activity
private void startActivity (int messageType, String encryptedMessage) {
if (messageType == REQ_SALE) {
Intent intent = new Intent(ACTION_SALE); // ACTION_SALE = "com.finexuscards.yippiepos.xxx.SALES"
intent.putExtra("Data", encryptedMessage);
startActivityForResult(intent, REQ_SALE); // REQ_SALE = 0
} else if (messageType == REQ_VOID) {
Intent intent = new Intent(ACTION_VOID); // ACTION_VOID = "com.finexuscards.yippiepos.xxx.VOID"
intent.putExtra("Data", encryptedMessage);
startActivityForResult(intent, REQ_VOID); // REQ_VOID = 1
} else if (messageType == REQ_REFUND) {
Intent intent = new Intent(ACTION_REFUND); // ACTION_VOID = "com.finexuscards.yippiepos.xxx.REFUND"
intent.putExtra("Data", encryptedMessage);
startActivityForResult(intent, REQ_REFUND); // REQ_REFUND = 2
}
}

// get response from onActivityResult()
protected void onActivityResult(int requestCode,int resultCode,Intent data){
super.onActivityResult(requestCode,resultCode,data);
String encryptedResponse = data.getStringExtra("Data");
}

Step 7 : Decrypt Response Message

Decrypt the response message retrieved from the Kayaaku POS application using the decryptWithFormatJWE function.

public static String decryptWithFormatJWE(SecretKey secretKey, String message) {
String decryptedMessage = "";
try {
// parse message to Object
JWEObject jweObject = JWEObject.parse(message);

// get header
JWEAlgorithm algorithm = jweObject.getHeader().getAlgorithm();
EncryptionMethod encryptionMethod = jweObject.getHeader().getEncryptionMethod();

// decrypt
if (algorithm.equals(JWEAlgorithm.DIR)) {
if (encryptionMethod == EncryptionMethod.A128CBC_HS256 || encryptionMethod == EncryptionMethod.A128GCM) {
Provider bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance();
Security.addProvider(bouncyCastleProvider);

// start decrypting
jweObject.decrypt(new DirectDecrypter(secretKey));
decryptedMessage = jweObject.getPayload().toString();
} else {
Log.d("Tag", "Unsupported other encryption method:" + encryptionMethod);
}
} else {
Log.d("Tag", "Unsupported other algorithms");
}
} catch (Exception ex) {
Log.e("Tag", "decryptedMessage Failed:" + ex.getMessage());
ex.printStackTrace();
}
return decryptedMessage;
}