Sign & Encrypt Data
Overview
Note: These are optional helper methods for your app, to use only if you need them. They are not part of the core MPoC card reading and payment flow: a transaction does not call them, and you do not need them to accept payments.
Two helpers for data your app needs to protect with the SDK's keys: signing with the SDK's per-device key, and
encrypting a payload with a card working key. Both are functions of MPoCAPI, are synchronous, and may wait for the
SDK to become ready on a cold process, so do not call them on the main thread. The SDK has to be initialized first,
see Init SDK.
Methods
MPoCAPI.generateSignature
Signs rawData with the SDK's per-device signing key (SHA256withRSA). The certificate that holds the matching public
key is SdkInfo.signCert, returned by MPoCAPI.getSdkInfo(), so your backend can verify the signature with it. The
backend can also fetch the same certificate from the AMS API, in the signCert field of the Query SDK Information response.
fun generateSignature(rawData: ByteArray): MPoCResult<ByteArray>Parameters
| Name | Type | Condition | Description |
|---|---|---|---|
rawData | ByteArray | Required | The bytes to sign, signed exactly as given. |
Returns
| Return Type | Description |
|---|---|
MPoCResult<ByteArray> | Success carries the raw signature bytes. Failure carries the error code and message. |
// off the main thread
when (val result = MPoCAPI.generateSignature("order-1234".toByteArray())) {
is MPoCResult.Success -> {
val signature: ByteArray = result.data
// send the data and Base64.encodeToString(signature, Base64.NO_WRAP) to your backend
}
is MPoCResult.Failure -> {
// result.code, result.message
}
}Verifying (backend). Read the public key from the signCert certificate and verify the signature over the same
bytes with SHA256withRSA. Your backend can also get the certificate itself, without the app sending it, from the signCert
field of the Query SDK Information response.
MPoCAPI.encryptData
Encrypts data with a card working key derived at a new KSN ("KSN2"), which is separate from the KSN the SDK uses for
the sale's track 2 ("KSN1"). The bytes are encrypted exactly as given, so there is no encoding step: your app decides how
its own data becomes bytes (for a string, text.toByteArray() is UTF-8), and the backend decodes the decrypted bytes the
same way.
const val MAX_ENCRYPT_DATA_BYTES = 4096
fun encryptData(
data: ByteArray,
mode: EncryptMode = EncryptMode.CBC
): MPoCResult<MhdEncryptedData>Parameters
| Name | Type | Condition | Description |
|---|---|---|---|
data | ByteArray | Required | 1 to MPoCAPI.MAX_ENCRYPT_DATA_BYTES (4096) bytes. Anything else returns MPoCErrorCode.ErrInvalidParameters (41500). |
mode | EncryptMode | Optional | EncryptMode.CBC (default, PKCS7 padding) or EncryptMode.GCM. See EncryptMode. |
Returns
| Return Type | Description |
|---|---|
MPoCResult<MhdEncryptedData> | Success carries the encrypted data and its KSN. Failure carries the error code and message. |
data class MhdEncryptedData(
// hex of IV || ciphertext (CBC, 16-byte IV) or IV || ciphertext || tag (GCM, 12-byte IV, 16-byte tag).
// The IV is random on every call
val encryptedHex: String,
// the 12-byte KSN (24 hex characters) the key was derived at
val ksn: String,
// the mode that was used
val mode: EncryptMode
)// off the main thread
when (val result = MPoCAPI.encryptData("hello".toByteArray(), EncryptMode.GCM)) {
is MPoCResult.Success -> {
// send result.data.encryptedHex and result.data.ksn to your backend
}
is MPoCResult.Failure -> {
// result.code, result.message
}
}Decrypting (backend). Derive the card working key from the BDK (or the IPEK) and the returned ksn, with DUKPT-AES
key usage DataEncryptionBothWays (the same derivation as for track 2). Split the IV off the front of the decoded hex,
then decrypt.
Behaviour to know
- Each call consumes one KSN from the DUKPT counter and never reuses one, including across calls from different threads and after a process restart. The counter allows about 4.29 billion values. When it is spent the call fails and nothing changes.
- It needs no sale and no earlier call. An app that has only initialized the SDK can call it, and the sale's own card key (the one used for track 2) is not touched.
- Once the payment keys are provisioned it works without network. If they are missing or expired, the SDK downloads them first, the same way a sale does.