Extension & Util
Here are some general util/ extension functions (in kotlin):
Crypto
AES
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object Aes {
private const val TAG = "AES"
enum class Padding {
NoPadding,
PKCS5Padding,
// PKCS1Padding,
// PKCS7Padding
}
fun encryptEcb(data: ByteArray, key: ByteArray, padding: Padding): ByteArray {
val keySpec = SecretKeySpec(key, "AES")
try {
val cipher = Cipher.getInstance("AES/ECB/${padding}")
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
return cipher.doFinal(data)
} catch (e: Exception) {
Log.d(TAG, "AES-ECB encrypt failed: $e")
throw e
}
}
fun decryptEcb(data: ByteArray, key: ByteArray, padding: Padding): ByteArray {
val keySpec = SecretKeySpec(key, "AES")
try {
val cipher = Cipher.getInstance("AES/ECB/${padding}")
cipher.init(Cipher.DECRYPT_MODE, keySpec)
return cipher.doFinal(data)
} catch (e: Exception) {
Log.d(TAG, "AES-ECB decrypt failed: $e")
throw e
}
}
fun encrypt(data: ByteArray, key: ByteArray, padding: Padding, iv: ByteArray): ByteArray {
val keySpec = SecretKeySpec(key, "AES")
try {
val cipher = Cipher.getInstance("AES/CBC/${padding}")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
return cipher.doFinal(data)
} catch (e: Exception) {
Log.d(TAG, "AES-CBC encrypt failed: $e")
throw e
}
}
fun decrypt(data: ByteArray, key: ByteArray, padding: Padding, iv: ByteArray): ByteArray {
val keySpec = SecretKeySpec(key, "AES")
try {
val cipher = Cipher.getInstance("AES/CBC/${padding}")
cipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
return cipher.doFinal(data)
} catch (e: Exception) {
Log.d(TAG, "AES-CBC decrypt failed: $e")
throw e
}
}
}
DUKPT AES (Host)
/**
* ANSI X9.24-3-2017
* 6.3.2 Derivation Data, Table 2 & 3 Derivation Data
* @param algoIndicator Indicates the algorithm that is going to use the derived key.
* @param length Length, in bits, of the keying material being generated.
*/
enum class KeyType(val algoIndicator: String, val length: String) {
//`2TDEA`("0000", "0080"),
//`3TDEA`("0001", "00C0"),
AES128("0002", "0080"),
AES192("0003", "00C0"),
AES256("0004", "0100"),
}
/**
* ANSI X9.24-3-2017
* 6.3.2 Derivation Data, Table 2 & 3 Derivation Data
* keyUsageIndicator: Indicates how the key to be derived is to be used
*/
enum class KeyUsage(val usageIndicator: String) {
KeyEncryptionKey("0002"),
PinEncryption("1000"),
MessageAuthenticationGeneration("2000"),
MessageAuthenticationVerification("2001"),
MessageAuthenticationBothWays("2002"),
DataEncryptionEncrypt("3000"),
DataEncryptionDecrypt("3001"),
DataEncryptionBothWays("3002"),
KeyDerivation("8000"),
KeyDerivationInitialKey("8001")
}
/**
* AES DUKPT ANSI X9.24-3-2017 for more details
* 6.4 Host Security Module Algorithm
*/
@OptIn(ExperimentalStdlibApi::class)
class DukptAesHost {
/**
* Derive functions should be stateless and hence in companion object
*/
companion object {
/**
* 6.3 key derivation function
*/
private fun deriveKey(
derivationKey: ByteArray,
keyType: KeyType,
derivationData: ByteArray,
): ByteArray {
val length = keyType.length.hexToInt()
val time = (length + 127) / 128
var result = byteArrayOf()
for (i in 1..time) {
val tmp = derivationData.copyOf().apply {
set(1, i.toByte())
}
result += Aes.encryptEcb(tmp, derivationKey, Padding.NoPadding)
}
return result.copyOfRange(0, length / 8)
}
/**
* 6.3.2 & 6.3.2 Create derivation data
*/
private fun createDerivationData(
keyUsage: KeyUsage,
keyType: KeyType,
initialKeyId: String,
hexCounter: String,
): ByteArray {
val version = "01"
val keyBlockCounter = "01"
val data = if (keyUsage == KeyUsage.KeyDerivationInitialKey) {
initialKeyId
} else {
initialKeyId.substring(8) + hexCounter.padStart(8, '0')
}
return "$version$keyBlockCounter${keyUsage.usageIndicator}${keyType.algoIndicator}${keyType.length}$data".hexToByteArray()
}
fun deriveInitialKeyByBdk(
bdk: ByteArray,
keyType: KeyType,
initialKeyId: String,
): ByteArray {
val derivationData = createDerivationData(KeyUsage.KeyDerivationInitialKey, keyType, initialKeyId, "")
return deriveKey(bdk, keyType, derivationData)
}
fun deriveWorkingKeyByBdk(
bdk: ByteArray,
bdkKeyType: KeyType,
workingKeyType: KeyType,
workingKeyUsage: KeyUsage,
ksn: String,
): ByteArray {
val initialKey = deriveInitialKeyByBdk(bdk, bdkKeyType, ksn.substring(0..15))
return deriveWorkingKeyByInitialKey(initialKey, bdkKeyType, workingKeyUsage, workingKeyType, ksn)
}
fun deriveWorkingKeyByInitialKey(
initialKey: ByteArray,
deriveKeyType: KeyType,
workingKeyUsage: KeyUsage,
workingKeyType: KeyType,
ksn: String,
): ByteArray {
require(ksn.length == 24)
val initialKeyId = ksn.substring(0..15)
val transactionCounter = ksn.substring(16..23)
// set the most significant bit to one and all other bits to zero
var mask = 0x80000000
var workingCounter = 0L
var derivationKey = initialKey
// calculate current derivation key from initial key
while (mask > 0) {
if (mask.and(transactionCounter.toLong(16)) != 0L) {
workingCounter = workingCounter.or(mask)
val derivationData = createDerivationData(
KeyUsage.KeyDerivation,
deriveKeyType,
initialKeyId,
workingCounter.toString(16)
)
derivationKey = deriveKey(derivationKey, deriveKeyType, derivationData)
}
mask = mask shr 1
}
// derive working key from current derivation key
val derivationData = createDerivationData(workingKeyUsage, workingKeyType, initialKeyId, transactionCounter)
return deriveKey(derivationKey, workingKeyType, derivationData)
}
}
}
PIN Block
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor
@OptIn(ExperimentalStdlibApi::class)
object PinBlock {
/**
* reference: https://www.eftlab.com/knowledge-base/complete-list-of-pin-blocks
* common PIN-block formats based on ISO 9564
*/
enum class PinBlockFormat(
val nibble: Int,
) {
Iso0(nibble = 0),
Iso1(nibble = 1),
Iso2(nibble = 2),
Iso3(nibble = 3),
Iso4(nibble = 4),
}
/**
* Prepares the PIN using the specified PIN block format.
*
* @param rawPin The raw PIN entered by the user.
* @param pinBlockFormat The format of the PIN block.
* @return The prepared PIN as a hexadecimal string.
* @throws IllegalArgumentException If the length of the PIN is less than 4.
*/
private fun preparePin(rawPin: String, pinBlockFormat: PinBlockFormat): String {
require(rawPin.length >= 4) { "PIN length must be >= 4" }
val randomBytes = ByteArray(8).apply { SecureRandom().nextBytes(this) }
return when (pinBlockFormat) {
PinBlockFormat.Iso0,
PinBlockFormat.Iso2,
-> "${pinBlockFormat.nibble}${rawPin.length}$rawPin".padEnd(16, 'f')
PinBlockFormat.Iso1,
PinBlockFormat.Iso3,
-> "${pinBlockFormat.nibble}${rawPin.length}$rawPin".plus(randomBytes.toHexString()).take(16)
PinBlockFormat.Iso4 -> "${pinBlockFormat.nibble}${rawPin.length}$rawPin".padEnd(16, 'a').plus(randomBytes.toHexString()).take(32)
}
}
/**
* Prepares the PAN (Primary Account Number) for xor or encryption using the specified PIN block format.
*
* @param rawPan The raw PAN entered by the user.
* @param pinBlockFormat The format of the PIN block.
* @return The prepared PAN as a string.
* @throws IllegalArgumentException If the length of the PAN is not in the range of 12 to 19.
*/
private fun preparePan(rawPan: String, pinBlockFormat: PinBlockFormat): String {
require(rawPan.length in 12..19) { "PAN length must be in 12..19" }
// only iso0, iso3, iso4 use the pan for xor or aes
// but to avoid nullable type just return the same formatted pan for iso1 & 2
return when (pinBlockFormat) {
PinBlockFormat.Iso0,
PinBlockFormat.Iso1,
PinBlockFormat.Iso2,
PinBlockFormat.Iso3,
-> "0000".plus(rawPan.dropLast(1).takeLast(12))
PinBlockFormat.Iso4 -> {
// PAN pad length indicating PAN length of 12 plus the value of the field ‘0’-‘7’ (ranging then from 12 to 19)
val lengthAbove12 = rawPan.length - 12
"$lengthAbove12$rawPan".padEnd(32, '0')
}
}
}
/**
* Calculates the PIN block based on the provided parameters.
*
* @param rawPin The raw PIN entered by the user.
* @param pinBlockFormat The format of the PIN block.
* @param rawPan The raw PAN (Primary Account Number) used for Iso0, Iso3, or Iso4 formats.
* @param pinKey The key used for encrypting the PIN block in Iso4 format.
* @return The calculated PIN block as a hexadecimal string.
*
* @throws IllegalArgumentException If the PAN is required but not provided for Iso0, Iso3, or Iso4 formats.
* @throws IllegalArgumentException If the PIN key is required but not provided for Iso4 format.
*/
fun getPinBlock(
rawPin: String,
pinBlockFormat: PinBlockFormat,
rawPan: String? = null,
pinKey: ByteArray? = null,
): String {
if (pinBlockFormat in arrayOf(PinBlockFormat.Iso0, PinBlockFormat.Iso3, PinBlockFormat.Iso4)) {
require(rawPan != null) { "Require PAN for Iso0, Iso3, or Iso4" }
}
val pinBytes = preparePin(rawPin, pinBlockFormat).hexToByteArray()
val panBytes = rawPan?.let { preparePan(it, pinBlockFormat).hexToByteArray() }
if (pinBlockFormat in arrayOf(PinBlockFormat.Iso0, PinBlockFormat.Iso3)) {
return pinBytes
.mapIndexed { idx, byte -> byte xor panBytes!![idx] }
.toByteArray()
.toHexString()
}
// ISO 9564-1: 2017 Format 4.
if (pinBlockFormat == PinBlockFormat.Iso4) {
require(pinKey != null) { "Require PIN key for Iso4" }
return pinBytes
// 3. PIN block is encrypted with AES key - Format 4 uses AES-128 ECB
.run { Aes.encryptEcb(this, pinKey, Aes.Padding.NoPadding) }
// 4. The resulting Intermediate Block A is then XOR’ed with PAN Block
.mapIndexed { idx, byte -> byte xor panBytes!![idx] }.toByteArray()
// 5. The resulting Intermediate Block B is enciphered with the AES key again to get final Enciphered PIN Block
.run { Aes.encryptEcb(this, pinKey, Aes.Padding.NoPadding).toHexString() }
}
// PinBlockFormat.Iso1, PinBlockFormat.Iso2
return pinBytes.toHexString()
}
/**
* Decrypts the ISO 9564-1 Format 4 encrypted PIN block into plain PIN string.
* Warning: DO NOT do this anywhere, you'll always want the PIN block encrypted
* This is just for demonstration purpose
*
* @param pinKey The key used for PIN encryption. It should be a 16-byte AES key.
* @param epb The encrypted PIN block as a byte array.
* @param pan The PAN (Primary Account Number) as a string.
* @return The decrypted plain PIN.
*/
fun dangerouslyDecryptIso4EpbToPin(pinKey: ByteArray, epb: ByteArray, pan: String): String {
val cipher = Cipher
.getInstance("AES/ECB/NoPadding")
.apply { init(Cipher.DECRYPT_MODE, SecretKeySpec(pinKey, "AES")) }
val panBytes = preparePan(pan, PinBlockFormat.Iso4).hexToByteArray()
return cipher
// decrypt epb to block B
.doFinal(epb)
// reverse xor with pan, for block a
.mapIndexed { index, byte -> byte xor panBytes[index] }.toByteArray()
// decrypt again for plain pin block
.run { cipher.doFinal(this).toHexString() }
// extract pin string
.run { drop(2).take(substring(1, 2).toInt(16)) }
}
}
Android
Context
Load JSON from android asset directory
inline fun <reified T> Context.loadJsonFromAsset(fileName: String): T {
val jsonStr = applicationContext.assets
.open(fileName)
.bufferedReader()
.use { it.readText() }
val type = object : TypeToken<T>() {}.type
return Gson().fromJson(jsonStr, type)
}
Find activity from context
tailrec fun Context.findActivity(): ComponentActivity = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.findActivity()
else -> throw IllegalStateException("no activity")
}