Embed Code
- Java
- Kotlin
- Xamarin
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 : Declare Input
Declare all input parameters in the request and create a JSON string.
- 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
- Refund
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 = UtilCrypto.encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage);
startActivity(REQ_VOID, jsonMessageEncrypted);
}
public void receiveMessage(ReadableMap data) {
String msgVer = data.getString("msgVer");
String callerDevice = data.getString("callerDevice");
String callerDeviceVer = data.getString("callerDeviceVer");
String refundTxnID = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
String oriTxnID = data.getString("oriTxnID");
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 jsonMessage = "{'MsgVer' : '" + msgVer +
"', 'CallerDeviceType' : '" + callerDevice +
"', 'CallerDeviceVer' : '" + callerDeviceVer +
"', 'RefundTxnID' : '" + refundTxnID +
"', 'OriTxnID' : '" + oriTxnID +
"', 'LocalTxnDTTime' : '" + txnDateTime +
"', 'Email' : '" + email +
"', 'Pwd' : '" + password +
"', 'RefundAmt' : '" + amt +
"', 'CrcyTxn' : '" + crcyCde + "'}";
ECKey publicKeyASJWK = new ECKey.Builder(Curve.P_256, thirdAppPublicKey)
.keyID(UUID.randomUUID().toString())
.build();
String jsonMessageEncrypted = UtilCrypto.encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage);
startActivity(REQ_REFUND, jsonMessageEncrypted);
}
Step 3 : 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 4 : 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.
// 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 5 : Private and Public Keys
Generate both the public and private keys.
Step 5.1 : Generate Keys
Please refer to Appendix 4 to generate EC Keypair.
Step 5.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 = UtilCrypto.loadECPrivateKey(reactContext.getAssets().open("ec_private_key_pkcs8.pem"));
thirdAppPublicKey = UtilCrypto.loadECPublicKey(reactContext.getAssets().open("ec_public_key.pem"));
yippiePosPublicKey = UtilCrypto.loadECPublicKey(reactContext.getAssets().open("yippiepos_public_key.key"));
String refNum = "1.0";
secretKey = UtilCrypto.generateECDHSecret(thirdAppPrivateKey, yippiePosPublicKey, refNum);
} catch (IOException ex) {
ex.printStackTrace();
}
BA = BluetoothAdapter.getDefaultAdapter();
}
Step 6 : Create Class
Create a new Java class named UtilCrypto
for the encryption and decryption functions.
Step 7 : Load the Initialized Keys
Load the initialized keys from the key file.
- Private Key
- Public Key
- ECDH Secret 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;
}
public static ECPublicKey loadECPublicKey(InputStream certFile) {
ECPublicKey ecPublicKey = null;
PublicKey publicKey = null;
try {
byte[] publicKeyAsBytes = new byte[certFile.available()];
certFile.read(publicKeyAsBytes);
certFile.close();
try {
String certFileContent = new String(publicKeyAsBytes)
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "");
publicKeyAsBytes = android.util.Base64.decode(certFileContent, android.util.Base64.DEFAULT);
} catch (Exception ex) {
ex.printStackTrace();
}
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyAsBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
publicKey = keyFactory.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
e.printStackTrace();
}
if (publicKey instanceof ECPublicKey) {
ecPublicKey = (ECPublicKey) publicKey;
}
String test = String.valueOf(publicKey);
return ecPublicKey;
}
public static SecretKey generateECDHSecret(ECPrivateKey sdkPrivateKey, ECPublicKey thirdAppPublicKey, String partyVInfo) {
SecretKey secretKey = null;
try{
secretKey = ECDH.deriveSharedSecret(thirdAppPublicKey, sdkPrivateKey, null);
if(partyVInfo != null){
ConcatKDF kdf = new ConcatKDF("SHA-256");
int length = ECDH.sharedKeyLength(JWEAlgorithm.ECDH_ES,
EncryptionMethod.A128CBC_HS256);
if(partyVInfo.isEmpty()){
secretKey = kdf.deriveKey(secretKey, length, null, null, null, null, null);
} else {
secretKey = kdf.deriveKey(
secretKey,
length,
ConcatKDF.encodeDataWithLength(new byte[0]),
ConcatKDF.encodeDataWithLength(new byte[0]),
ConcatKDF.encodeDataWithLength(partyVInfo.getBytes()),
ConcatKDF.encodeIntData(length),
ConcatKDF.encodeNoData());
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return secretKey;
}
Step 8 : 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;
}
Step 1 : Implement ECC Libraries
implementation("com.nimbusds:nimbus-jose-jwt:8.9")
implementation("org.bouncycastle:bcprov-jdk15on:1.64")
Step 2 : Declare Input
Declare all input parameters and create a JSON string.
- 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
- Refund
fun receiveMessage(data: ReadableMap) {
val msgVer = data.getString("msgVer")
val callerDevice = data.getString("callerDevice")
val callerDeviceVer = data.getString("callerDeviceVer")
val txnID = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase()
val txnDateTime = data.getString("txnDateTime")
val email = data.getString("email")
val password = data.getString("password")
val crcyCde = data.getString("crcyCde")
val amt = data.getString("amt")
val stan = data.getString("stan")
val authIdResp = data.getString("authIdResp")
val rrn = data.getString("rrn")
val mrn = data.getString("mrn")
val 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"
}
""".trimIndent()
fun receiveMessage(data: ReadableMap) {
val msgVer = data.getString("msgVer")
val callerDevice = data.getString("callerDevice")
val callerDeviceVer = data.getString("callerDeviceVer")
val refundTxnID = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase()
val oriTxnID = data.getString("oriTxnID")
val txnDateTime = data.getString("txnDateTime")
val email = data.getString("email")
val password = data.getString("password")
val crcyCde = data.getString("crcyCde")
val amt = data.getString("amt")
val jsonMessage = """
{
"MsgVer": "$msgVer",
"CallerDeviceType": "$callerDevice",
"CallerDeviceVer": "$callerDeviceVer",
"RefundTxnID": "$refundTxnID",
"OriTxnID": "$oriTxnID",
"LocalTxnDTTime": "$txnDateTime",
"Email": "$email",
"Pwd": "$password",
"RefundAmt": "$amt",
"CrcyTxn": "$crcyCde"
}
""".trimIndent()
Step 3 : Encrypt String
After getting the jsonMessage
in string, encrypt the string with function encryptWithFormatJWE
.
val publicKeyASJWK = ECKey.newBuilder(Curve.P_256, thirdAppPublicKey)
.keyID(UUID.randomUUID().toString())
.build()
val jsonMessageEncrypted = UtilCrypto.encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage)
}
Step 4 : 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.
if (mMessageType == REQ_SALE) {
val intent = Intent(ACTION_SALE)
intent.putExtra("Data", encryptedMessage)
startActivityForResult(intent, REQ_SALE)
} else if (mMessageType == REQ_VOID) {
val intent = Intent(ACTION_VOID)
intent.putExtra("Data", encryptedMessage)
startActivityForResult(intent, REQ_VOID)
} else if (mMessageType == REQ_REFUND) {
val intent = Intent(ACTION_REFUND)
intent.putExtra("Data", encryptedMessage)
startActivityForResult(intent, REQ_REFUND)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val encryptedResponse = data?.getStringExtra("Data")
}
Step 5 : Private and Public Keys
Generate both the public and private keys.
Step 5.1 : Generate Keys
Please refer to Appendix 4 to generate EC Keypair.
Step 5.2 : Initialize Keys
Initialize the keys that will be used for the encryption and decryption process.
constructor(reactContext: ReactApplicationContext) : super(reactContext) {
this.reactContext = reactContext
try {
thirdAppPrivateKey = UtilCrypto.loadECPrivateKey(reactContext.getAssets().open("ec_private_key_pkcs8.pem"))
thirdAppPublicKey = UtilCrypto.loadECPublicKey(reactContext.getAssets().open("ec_public_key.pem"))
yippiePosPublicKey = UtilCrypto.loadECPublicKey(reactContext.getAssets().open("yippiepos_public_key.key"))
val refNum = "1.0"
secretKey = UtilCrypto.generateECDHSecret(thirdAppPrivateKey, yippiePosPublicKey, refNum)
} catch (ex: IOException) {
ex.printStackTrace()
}
BA = BluetoothAdapter.getDefaultAdapter()
}
Step 6 : Create Class
Create a new Java class named UtilCrypto
for the encryption and decryption functions.
Step 7 : Load the Initialized Keys
Load the initialized keys from the key file.
- Private Key
- Public Key
- ECDH Secret Key
fun loadECPrivateKey(certFile: InputStream): ECPrivateKey {
var ecPrivateKey: ECPrivateKey? = null
var privateKey: PrivateKey? = null
try {
val encodedPrivateKey = ByteArray(certFile.available())
certFile.read(encodedPrivateKey)
certFile.close()
try {
val certFileContent = 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 (ex: Exception) {
ex.printStackTrace()
}
val privateKeySpec = PKCS8EncodedKeySpec(encodedPrivateKey)
val keyFactory = KeyFactory.getInstance("EC")
privateKey = keyFactory.generatePrivate(privateKeySpec)
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
e.printStackTrace()
}
if (privateKey is ECPrivateKey) {
ecPrivateKey = privateKey
}
return ecPrivateKey
}
fun loadECPublicKey(certFile: InputStream): ECPublicKey {
var ecPublicKey: ECPublicKey? = null
var publicKey: PublicKey? = null
try {
val encodedPublicKey = ByteArray(certFile.available())
certFile.read(encodedPublicKey)
certFile.close()
try {
val certFileContent = String(encodedPublicKey)
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
encodedPublicKey = android.util.Base64.decode(certFileContent, android.util.Base64.DEFAULT)
} catch (ex: Exception) {
ex.printStackTrace()
}
val publicKeySpec = X509EncodedKeySpec(encodedPublicKey)
val keyFactory = KeyFactory.getInstance("EC")
publicKey = keyFactory.generatePublic(publicKeySpec)
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
e.printStackTrace()
}
if (publicKey is ECPublicKey) {
ecPublicKey = publicKey
}
return ecPublicKey
}
fun generateECDHSecret(sdkPrivateKey: ECPrivateKey,
thirdAppPublicKey: ECPublicKey,
partyVInfo: String): SecretKey {
var secretKey: SecretKey? = null
try {
secretKey = ECDH.deriveSharedSecret(thirdAppPublicKey, sdkPrivateKey, null)
if (partyVInfo != null) {
val kdf = ConcatKDF("SHA-256")
val length = ECDH.sharedKeyLength(JWEAlgorithm.ECDH_ES,
EncryptionMethod.A128CBC_HS256)
if (partyVInfo.isEmpty()) {
secretKey = kdf.deriveKey(
secretKey,
length,
null,
null,
null,
null,
null)
} else {
secretKey = kdf.deriveKey(
secretKey,
length,
ConcatKDF.encodeDataWithLength(new byte[0]),
ConcatKDF.encodeDataWithLength(new byte[0]),
ConcatKDF.encodeDataWithLength(partyVInfo.toByteArray()),
ConcatKDF.encodeIntData(length),
ConcatKDF.encodeNoData())
}
}
} catch (ex: Exception) {
ex.printStackTrace()
}
return secretKey
}
Step 8 : Encrypt Request Message
Encrypt the request message to be sent to the Kayaaku POS application using the encryptWithFormatJWE
function.
fun encryptWithFormatJWE(transactionID: String, sdkPublicKeyAsJWK: String, secretKey: SecretKey, jweAlgorithm: JWEAlgorithm, message: String): String {
val encryptedMessage = ""
try {
val builder = JWEHeader.Builder(jweAlgorithm, EncryptionMethod.A128CBC_HS256)
if (!TextUtils.isEmpty(transactionID)) {
builder.keyID(transactionID)
}
if (!TextUtils.isEmpty(sdkPublicKeyAsJWK)) {
builder.ephemeralPublicKey(ECKey.parse(sdkPublicKeyAsJWK))
}
val header = builder.build()
if (jweAlgorithm == JWEAlgorithm.DIR) {
val bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance()
Security.addProvider(bouncyCastleProvider)
val jweObject = JWEObject(header, Payload(message))
jweObject.encrypt(DirectEncrypter(secretKey))
encryptedMessage = jweObject.serialize()
} else if (jweAlgorithm == JWEAlgorithm.ECDH_ES) {
val jweParts = ContentCryptoProvider.encrypt(header, message.toByteArray(),
secretKey, null, JWEJCAContext())
val stringBuilder = StringBuilder(header.toBase64URL().toString())
stringBuilder.append('.')
if (jweParts.getEncryptedKey() != null) {
stringBuilder.append(jweParts.getEncryptedKey().toString())
}
stringBuilder.append('.')
if (jweParts.getInitializationVector() != null) {
stringBuilder.append(jweParts.getInitializationVector().toString())
}
stringBuilder.append('.')
stringBuilder.append(jweParts.getCipherText().toString())
stringBuilder.append('.')
if (jweParts.getAuthenticationTag() != null) {
stringBuilder.append(jweParts.getAuthenticationTag().toString())
}
encryptedMessage = stringBuilder.toString()
} else {
Log.d("Tag", "Unsupported other algorithms")
}
}
Step 9 : Decrypt Response Message
Decrypt the response message retrieved from the Kayaaku POS application using the decryptWithFormatJWE
function.
fun decryptWithFormatJWE(secretKey: SecretKey, message: String): String {
val decryptedMessage = ""
try {
// parse message to Object
val jweObject = JWEObject.parse(message)
// get header
val algorithm = jweObject.getHeader().getAlgorithm()
val encryptionMethod = jweObject.getHeader().getEncryptionMethod()
// decrypt
if (algorithm.equals(JWEAlgorithm.DIR)) {
if (encryptionMethod == EncryptionMethod.A128CBC_HS256
|| encryptionMethod == EncryptionMethod.A128GCM) {
val 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
}
Step 1 : Download Library
- System.IdentityModel.Tokens.Jwt by Microsoft
- BouncyCastle.Cryptography by Legion of the Bouncy Castle Inc.
- jose-twt by Dmitry Vsekhvalnov
Step 2 : Declare Input
Declare all input parameters and create a JSON string.
- 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
- Refund
public void receiveMessage(ReadableMap data) {
string msgVer = data.GetString("msgVer");
string callerDevice = data.GetString("callerDevice");
string callerDeviceVer = data.GetString("callerDeviceVer");
string txnID = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
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 + "'}";
byte[] thirdAppPublicKeyBytes = loadECPublicKeyBytes(assets.Open("ec_public_key.pem"));
var pubKeyCoordinates = GetPublicKeyCoordinates(thirdAppPublicKeyBytes);
var sdkPublicKeyAsJWK = new JwtPayload
{
{"kty", "EC"},
{"crv", "P-256"},
{"kid", Guid.NewGuid().ToString()},
{"x", pubKeyCoordinates.X},
{"y", pubKeyCoordinates.Y}
};
string jsonMessageEncrypted = UtilCrypto.encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage);
startActivity(REQ_VOID, jsonMessageEncrypted);
}
public static ECCoordinate GetPublicKeyCoordinates(byte[] publicKeyBytes)
{
var asn1 = (Asn1Sequence)Asn1Object.FromByteArray(publicKeyBytes);
var at1 = (DerBitString)asn1[1];
var xyBytes = at1.GetBytes();
X9ECParameters x9 = ECNamedCurveTable.GetByName("P-256");
ECDomainParameters domainParams = new ECDomainParameters(x9.Curve, x9.G, x9.N, x9.H, x9.GetSeed());
ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(x9.Curve.DecodePoint(xyBytes), domainParams);
ECCoordinate ecCoordinate = new ECCoordinate
{
X= EncodeCordinate(publicKeyParams.Q.AffineXCoord.ToBigInteger()),
Y= EncodeCordinate(publicKeyParams.Q.AffineYCoord.ToBigInteger())
};
return ecCoordinate;
}
public static string EncodeCordinate(Org.BouncyCastle.Math.BigInteger integer)
{
var notPadded = integer.ToByteArray();
int bytesToOutput = (256 + 7) / 8;
if (notPadded.Length >= bytesToOutput)
return Jose.Base64Url.Encode(notPadded);
var padded = new byte[bytesToOutput];
Array.Copy(notPadded, 0, padded, bytesToOutput - notPadded.Length, notPadded.Length);
return Jose.Base64Url.Encode(padded);
}
public static byte[] loadECPublicKeyBytes(System.IO.Stream certFile)
{
MemoryStream memoryStream = new MemoryStream();
certFile.CopyTo(memoryStream);
byte[] publicKeyAsBytes = memoryStream.ToArray();
string certFileContent = System.Text.Encoding.UTF8.GetString(publicKeyAsBytes)
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "");
publicKeyAsBytes = Convert.FromBase64String(certFileContent);
return publicKeyAsBytes;
}
public void receiveMessage(ReadableMap data) {
string msgVer = data.GetString("msgVer");
string callerDevice = data.GetString("callerDevice");
string callerDeviceVer = data.GetString("callerDeviceVer");
string refundTxnID = Guid.NewGuid().ToString().Replace("-", "").ToUpper();
string oriTxnID = data.GetString("oriTxnID");
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 jsonMessage = "{'MsgVer' : '" + msgVer +
"', 'CallerDeviceType' : '" + callerDevice +
"', 'CallerDeviceVer' : '" + callerDeviceVer +
"', 'RefundTxnID' : '" + refundTxnID +
"', 'OriTxnID' : '" + oriTxnID +
"', 'LocalTxnDTTime' : '" + txnDateTime +
"', 'Email' : '" + email +
"', 'Pwd' : '" + password +
"', 'RefundAmt' : '" + amt +
"', 'CrcyTxn' : '" + crcyCde + "'}";
byte[] thirdAppPublicKeyBytes = loadECPublicKeyBytes(assets.Open("ec_public_key.pem"));
var pubKeyCoordinates = GetPublicKeyCoordinates(thirdAppPublicKeyBytes);
var sdkPublicKeyAsJWK = new JwtPayload
{
{"kty", "EC"},
{"crv", "P-256"},
{"kid", Guid.NewGuid().ToString()},
{"x", pubKeyCoordinates.X},
{"y", pubKeyCoordinates.Y}
};
string jsonMessageEncrypted = UtilCrypto.encryptWithFormatJWE(txnID,
publicKeyASJWK.toJSONString(), secretKey, JWEAlgorithm.DIR, jsonMessage);
startActivity(REQ_REFUND, jsonMessageEncrypted);
}
public static ECCoordinate GetPublicKeyCoordinates(byte[] publicKeyBytes)
{
var asn1 = (Asn1Sequence)Asn1Object.FromByteArray(publicKeyBytes);
var at1 = (DerBitString)asn1[1];
var xyBytes = at1.GetBytes();
X9ECParameters x9 = ECNamedCurveTable.GetByName("P-256");
ECDomainParameters domainParams = new ECDomainParameters(x9.Curve, x9.G, x9.N, x9.H, x9.GetSeed());
ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(x9.Curve.DecodePoint(xyBytes), domainParams);
ECCoordinate ecCoordinate = new ECCoordinate
{
X= EncodeCordinate(publicKeyParams.Q.AffineXCoord.ToBigInteger()),
Y= EncodeCordinate(publicKeyParams.Q.AffineYCoord.ToBigInteger())
};
return ecCoordinate;
}
public static string EncodeCordinate(Org.BouncyCastle.Math.BigInteger integer)
{
var notPadded = integer.ToByteArray();
int bytesToOutput = (256 + 7) / 8;
if (notPadded.Length >= bytesToOutput)
return Jose.Base64Url.Encode(notPadded);
var padded = new byte[bytesToOutput];
Array.Copy(notPadded, 0, padded, bytesToOutput - notPadded.Length, notPadded.Length);
return Jose.Base64Url.Encode(padded);
}
public static byte[] loadECPublicKeyBytes(System.IO.Stream certFile)
{
MemoryStream memoryStream = new MemoryStream();
certFile.CopyTo(memoryStream);
byte[] publicKeyAsBytes = memoryStream.ToArray();
string certFileContent = System.Text.Encoding.UTF8.GetString(publicKeyAsBytes)
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "");
publicKeyAsBytes = Convert.FromBase64String(certFileContent);
return publicKeyAsBytes;
}
Step 3 : Encrypt Request Message
Encrypt the request message using the encryptWithFormatJWE
function.
public static string EncryptWithFormatJWE(string transactionID, JwtPayload sdkPublicKeyAsJWK, string secretKey, JweAlgorithm jweAlgorithm, string message)
{
string encryptedMessage = "";
try
{
var jwtHeader = new JwtHeader
{
{"epk", sdkPublicKeyAsJWK },
{"kid", transactionID},
};
byte[] secretKeyBytes = Convert.FromBase64String(secretKey);
encryptedMessage = JWT.Encode(message, secretKeyBytes, jweAlgorithm, JweEncryption.A128CBC_HS256, extraHeaders: jwtHeader);
}
catch (Exception ex)
{
}
return encryptedMessage;
}
Step 4 : Perform App-to-app Calling using INTENT
Use StartActivityForResult()
as per the sample code below.
Intent intent = new Intent("com.finexuscards.yippiepos.xxx.SALES");
intent.PutExtra("Data", jsonMessageEncrypted);
StartActivityForResult(intent, 0);
protected void OnActivityResult(int requestCode,int resultCode,Intent data){
base.OnActivityResult(requestCode, resultCode, data);
string encryptedResponse = data.GetStringExtra("Data");
}
Step 5 : Private and Public Keys
Generate both the public and private keys.
Step 5.1 : Generate Keys
Please refer to Appendix 4 to generate EC Keypair.
Step 5.2 : Initialize Keys
Initialize the keys that will be used for the encryption and decryption process.
ECPrivateKey thirdAppPrivateKey = loadECPrivateKey(assets.Open("ec_private_key_pkcs8.pem"));
ECPublicKey thirdAppPublicKey = loadECPublicKey(assets.Open("ec_public_key.pem"));
ECPublicKey yippiePosPublicKey = loadECPublicKey(assets.Open("yippiepos_public_key.key"));
Step 6 : Load the Initialized Keys
Load the initialized keys from the key file.
- Private Key
- Public Key
- ECDH Secret Key
public static ECPrivateKey loadECPrivateKey(System.IO.Stream certFile)
{
ECPrivateKey ecPrivateKey = null;
try {
MemoryStream memoryStream = new MemoryStream();
certFile.CopyTo(memoryStream);
byte[] encodedPrivateKey = memoryStream.ToArray();
try {
string certFileContent = System.Text.Encoding.UTF8.GetString(encodedPrivateKey)
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "");
encodedPrivateKey = Convert.FromBase64String(certFileContent);
}
catch (Exception ex) {
//to do …
}
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
KeyFactory keyFactory = KeyFactory.GetInstance("EC");
ecPrivateKey = (ECPrivateKey) keyFactory.GeneratePrivate(privateKeySpec);
}
catch (Exception e) {
//to do …
}
return ecPrivateKey;
}
public static ECPublicKey loadECPublicKey(System.IO.Stream certFile)
{
ECPublicKey ecPublicKey = null;
try
{
MemoryStream memoryStream = new MemoryStream();
certFile.CopyTo(memoryStream);
byte[] publicKeyAsBytes = memoryStream.ToArray();
try
{
string certFileContent = System.Text.Encoding.UTF8.GetString(publicKeyAsBytes)
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "");
publicKeyAsBytes = Convert.FromBase64String(certFileContent);
}
catch (Exception ex)
{
//to do …
}
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyAsBytes);
KeyFactory keyFactory = KeyFactory.GetInstance("EC");
ecPublicKey = (ECPublicKey) keyFactory.GeneratePublic(publicKeySpec);
}
catch (Exception e) {
//to do …
}
return ecPublicKey;
}
public static byte[]? DeriveSharedSecret(string privateKeyPath, string publicKeyPath)
{
using var privateKey = ECDiffieHellman.Create();
privateKey.ImportFromPem(File.ReadAllText(privateKeyPath).ToCharArray());
string publicKeyPem = File.ReadAllText(publicKeyPath);
byte[] publicKeyBytes = ConvertPemToDer(publicKeyPem);
using var publicKey = ECDiffieHellman.Create();
publicKey.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
byte[] sharedSecret = privateKey.DeriveRawSecretAgreement(publicKey.PublicKey);
return sharedSecret;
}
public static string GenerateSecretKey(byte[] sharedSecret, int keyLength, byte[] partyVInfo)
{
byte[] derivedKey = DeriveKey(
sharedSecret,
keyLength,
EncodeDataWithLength(new byte[0]),
EncodeDataWithLength(new byte[0]),
EncodeDataWithLengthReversed(partyVInfo),
EncodeIntData(keyLength),
EncodeNoData()
);
return Convert.ToBase64String(derivedKey);
}
public static byte[] DeriveKey(byte[] sharedSecret, int keyLength, byte[] algID, byte[] partyUInfo, byte[] partyVInfo, byte[] suppPubInfo, byte[] suppPrivInfo)
{
byte[] otherInfo = ComposeOtherInfo(algID, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo);
return DeriveKeyOtherInfo(sharedSecret, keyLength, otherInfo);
}
public static byte[] ComposeOtherInfo(byte[] algID, byte[] partyUInfo, byte[] partyVInfo, byte[] suppPubInfo, byte[] suppPrivInfo)
{
return algID.Concat(partyUInfo)
.Concat(partyVInfo)
.Concat(suppPubInfo)
.Concat(suppPrivInfo)
.ToArray();
}
public static byte[] DeriveKeyOtherInfo(byte[] sharedSecret, int keyLengthBits, byte[] otherInfo)
{
int keyLengthBytes = (int)Math.Ceiling(keyLengthBits / 8.0);
int hashRounds = (int)Math.Ceiling(keyLengthBytes / 32.0);
byte[] derivedKeyBuffer = new byte[hashRounds * 32];
using (SHA256 sha256 = SHA256.Create())
{
for (int i = 0; i < hashRounds; i++)
{
byte[] counter = BitConverter.GetBytes(i + 1);
Array.Reverse(counter);
byte[] hashInput = new byte[counter.Length + sharedSecret.Length + otherInfo.Length];
Buffer.BlockCopy(counter, 0, hashInput, 0, counter.Length);
Buffer.BlockCopy(sharedSecret, 0, hashInput, counter.Length, sharedSecret.Length);
Buffer.BlockCopy(otherInfo, 0, hashInput, counter.Length + sharedSecret.Length, otherInfo.Length);
byte[] hash = sha256.ComputeHash(hashInput);
Buffer.BlockCopy(hash, 0, derivedKeyBuffer, i * 32, Math.Min(32, derivedKeyBuffer.Length - i * 32));
}
}
byte[] finalKey = new byte[keyLengthBytes];
Array.Copy(derivedKeyBuffer, finalKey, keyLengthBytes);
return finalKey;
}
public static byte[] EncodeDataWithLength(byte[] data)
{
byte[] bytes = data ?? new byte[0];
byte[] length = BitConverter.GetBytes(bytes.Length);
byte[] result = new byte[length.Length + bytes.Length];
Array.Copy(length, 0, result, 0, length.Length);
Array.Copy(bytes, 0, result, length.Length, bytes.Length);
return result;
}
public static byte[] EncodeDataWithLengthReversed(byte[] data)
{
byte[] bytes = data ?? new byte[0];
byte[] length = BitConverter.GetBytes(bytes.Length);
Array.Reverse(length);
byte[] result = new byte[length.Length + bytes.Length];
Array.Copy(length, 0, result, 0, length.Length);
Array.Copy(bytes, 0, result, length.Length, bytes.Length);
return result;
}
public static byte[] EncodeIntData(int data)
{
byte[] bytes = BitConverter.GetBytes(data);
Array.Reverse(bytes);
return bytes;
}
public static byte[] EncodeNoData()
{
return new byte[0];
}
var secretKey = "";
string privateKeyPath = "ec_private_key_pkcs8.pem";
string publicKeyPath = "finexus_pos_public_key.key";
byte[]? sharedSecretByte = DeriveSharedSecret(privateKeyPath, publicKeyPath);
if (sharedSecretByte != null)
{
int keyLength = 512;
string partyVInfo = "1.0";
byte[] partyVInfoBytes = System.Text.Encoding.UTF8.GetBytes(partyVInfo);
secretKey = GenerateSecretKey(sharedSecretByte, keyLength, partyVInfoBytes);
}
Step 7 : Decrypt Response Message
Decrypt the response message retrieved from the Kayaaku POS application using the decryptWithFormatJWE
function.
public static string DecryptWithFormatJWE(string secretKey, string message)
{
string decryptedMessage = "";
try
{
byte[] secretKeyBytes = Convert.FromBase64String(secretKey);
decryptedMessage = JWT.Decode(message, secretKeyBytes, JweAlgorithm.DIR, JweEncryption.A128CBC_HS256);
}
catch (Exception ex)
{
}
return decryptedMessage;
}