Skip to content

Commit

Permalink
Merge pull request #5 from xendit/feat/implementation
Browse files Browse the repository at this point in the history
Fix: provide valid sessionId generation
  • Loading branch information
jidi1f authored Sep 6, 2022
2 parents 4916351 + c8f06f4 commit 65df688
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 81 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To be able to use XenCrypt, you will need to use a private key provided by Xendi
```android
import XenCrypt
try {
val xenKey = Base64.encode("BASE64_ENCODED_KEY_PROVIDED_BY_XENDIT".toByteArray())
val xenKey = Base64.encode("BASE64_ENCODED_KEY_PROVIDED_BY_XENDIT")
val xenCrypt = XenCrypt(xenKey);
// sessionKey - randomly generated 32 length string, use xenCrypt.getSessionKey(), or implement own
Expand Down
44 changes: 23 additions & 21 deletions app/src/main/java/XenCrypt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,20 @@ import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.Exception

data class SessionIdData(val iv: String, val sessionId: String)
data class SessionIdData(val iv: String, val encryptedSessionKey: String)

class XenCrypt constructor(publicKey: String?) {
class XenCrypt constructor(xenditKey: String) {
private val cipher: Cipher
private val publicKey: String
private val xenditKey: String
private val asymmetricCryptography: AsymmetricCryptography

init {
Security.addProvider(BouncyCastleProvider())
this.cipher = Cipher.getInstance(
"AES/CBC/PKCS7Padding",
BouncyCastleProvider.PROVIDER_NAME
"AES/CBC/PKCS7Padding"
)
val decodedPublicKey: String

this.asymmetricCryptography = AsymmetricCryptography(this.cipher)
try {
val byteArray = Base64.decode(publicKey, Base64.NO_WRAP)
decodedPublicKey = String(byteArray, StandardCharsets.UTF_8)

} catch (error: Exception) {
throw WrongPublicKeyError(error.message)
}

this.publicKey = decodedPublicKey
this.xenditKey = xenditKey
}

/**
Expand All @@ -45,10 +34,23 @@ class XenCrypt constructor(publicKey: String?) {
*/
fun generateSessionId(sessionKey: String): SessionIdData{
try {
val publicKey = asymmetricCryptography.getPublicKey(this.publicKey)
val sessionId = asymmetricCryptography.encrypt(sessionKey, publicKey)
val iv = this.ivKeyGenerator()
return SessionIdData(iv, sessionId);
val ivB64 = this.ivKeyGenerator()
val decodedKey: ByteArray = Base64.decode(
this.xenditKey,
Base64.NO_WRAP
) // use 32 characters session key generated at first step

val aesKey: SecretKey = SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")
val iv: ByteArray = Base64.decode(ivB64, Base64.NO_WRAP)
val ivSpec = IvParameterSpec(iv)

val aeseCipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
aeseCipher.init(Cipher.ENCRYPT_MODE, aesKey, ivSpec)
val utf8 = sessionKey.toByteArray(charset("UTF8"))
val encryptedSessionKey = aeseCipher.doFinal(utf8)
val encrypted = String(BASE64EncoderStream.encode(encryptedSessionKey))

return SessionIdData(ivB64, encrypted);
} catch (error: SessionIdError) {
throw SessionIdError(error.message)
}
Expand Down Expand Up @@ -96,7 +98,7 @@ class XenCrypt constructor(publicKey: String?) {
@Throws(Exception::class)
fun getSessionKey(): String {
val secureRandom = SecureRandom.getInstance("SHA1PRNG")
val byteArray = ByteArray(24)
val byteArray = ByteArray(32)
secureRandom.nextBytes(byteArray)
return String(Base64.encode(byteArray, Base64.NO_WRAP))
}
Expand Down
26 changes: 2 additions & 24 deletions app/src/main/java/utils/Util.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package utils

import android.util.Base64
import org.apache.commons.codec.binary.Base64.encodeBase64String as encodeBase64String
import java.io.UnsupportedEncodingException
import java.security.*
import java.security.spec.X509EncodedKeySpec
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
Expand All @@ -16,20 +16,6 @@ class AsymmetricCryptography(cipher: Cipher) {
this.cipher = cipher
}

@Throws(Exception::class)
fun getPublicKey(base64: String?): PublicKey {
val privateKeyPEM = base64
?.replace("-----BEGIN PUBLIC KEY-----", "")
?.replace(System.lineSeparator().toRegex(), "")
?.replace("\n","")
?.replace("-----END PUBLIC KEY-----", "")

val keyBytes = Base64.decode(privateKeyPEM, Base64.NO_WRAP)
val spec = X509EncodedKeySpec(keyBytes)
val kf = KeyFactory.getInstance("RSA")
return kf.generatePublic(spec)
}

@Throws(
NoSuchAlgorithmException::class,
NoSuchPaddingException::class,
Expand All @@ -41,15 +27,7 @@ class AsymmetricCryptography(cipher: Cipher) {
fun encrypt(sessionKey: String, key: PublicKey?): String {
this.cipher.init(Cipher.ENCRYPT_MODE, key)

return org.apache.commons.codec.binary.Base64.encodeBase64String(cipher.doFinal(sessionKey.toByteArray(charset("UTF-8"))))
}

@Throws(Exception::class)
fun getSessionKey(): String {
val secureRandom = SecureRandom.getInstance("SHA1PRNG")
val byteArray = ByteArray(24)
secureRandom.nextBytes(byteArray)
return String(Base64.encode(byteArray, Base64.NO_WRAP))
return encodeBase64String(cipher.doFinal(sessionKey.toByteArray(charset("UTF-8"))))
}

@Throws(
Expand Down
61 changes: 26 additions & 35 deletions app/src/test/java/com/example/xenissuing_android/ExampleUnitTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,14 @@ import org.junit.jupiter.api.Test
import utils.DecryptionError
import utils.EncryptionError
import utils.WrongPublicKeyError
import java.nio.charset.StandardCharsets
import java.security.*

fun encodeKey(keyBytes: ByteArray): String {
return String(java.util.Base64.getMimeEncoder().encode(keyBytes), StandardCharsets.UTF_8)
fun generateXenditKey(): String {
val secureRandom = SecureRandom.getInstance("SHA1PRNG")
val byteArray = ByteArray(32)
secureRandom.nextBytes(byteArray)
return String(Base64.encode(byteArray, Base64.NO_WRAP))
}

fun generatePublicKey(): String {
val kp: KeyPair?
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance("RSA")
kpg.initialize(2048)
kp = kpg.generateKeyPair()

val publicKey: PublicKey = kp.public
val publicKeyBytes: String = encodeKey(publicKey.getEncoded())

return addHeaders(publicKeyBytes)
}

fun addHeaders(key: String): String {
val headLine = "-----BEGIN PUBLIC KEY-----\n"
val footLine = "\n-----END PUBLIC KEY-----"
return headLine + key + footLine
}

class XenCryptUnitTest{
companion object {
init {
Expand Down Expand Up @@ -67,26 +50,29 @@ class XenCryptUnitTest{
}
}
}
val generatedRsaPublic = generatePublicKey()
var RSA_PUBLIC = java.util.Base64.getEncoder().encode(generatedRsaPublic.toByteArray())

@Test
@DisplayName("Test session-id generation")
@DisplayName("Test session-id data generation")
fun generateSessionId() {
val base64regex = Regex("^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?\$")
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenditKey = generateXenditKey()

val xenCrypt = XenCrypt(xenditKey)
val sessionKey = xenCrypt.getSessionKey()
val sessionData = xenCrypt.generateSessionId(sessionKey)

base64regex.matches(sessionData.sessionId)
assertEquals(base64regex.matches(sessionData.sessionId), true)
val decodedKey: ByteArray = Base64.decode(sessionData.encryptedSessionKey, Base64.NO_WRAP)
val decodedIv: ByteArray = Base64.decode(sessionData.iv, Base64.NO_WRAP)

assertEquals(decodedKey.size, 48)
assertEquals(decodedIv.size, 16)
}

@Test
@DisplayName("should decrypt plain text")
fun decrypt() {
val xenditKey = generateXenditKey()
val plain = "test"
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenCrypt = XenCrypt(xenditKey)
val sessionKey = xenCrypt.getSessionKey()
val iv = xenCrypt.ivKeyGenerator()
val encryptedSecret = xenCrypt.encryption(plain, sessionKey, iv)
Expand All @@ -98,8 +84,9 @@ class XenCryptUnitTest{
@Test
@DisplayName("should not decrypt plain text if provided different session key then was provided during encryption")
fun decryptWithWrongPrivatKey() {
val xenditKey = generateXenditKey()
val plain = "test"
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenCrypt = XenCrypt(xenditKey)
val privateEncryptionKey = xenCrypt.getSessionKey()
val privateDecryptionKey = xenCrypt.getSessionKey()
val iv = xenCrypt.ivKeyGenerator()
Expand All @@ -115,8 +102,9 @@ class XenCryptUnitTest{
@Test
@DisplayName("should not decrypt plain text if provided different iv then was provided during encryption")
fun decryptWithWrongIv() {
val xenditKey = generateXenditKey()
val plain = "test"
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenCrypt = XenCrypt(xenditKey)
val privateKey = xenCrypt.getSessionKey()
val iv = xenCrypt.ivKeyGenerator()
val secondIv = xenCrypt.ivKeyGenerator()
Expand All @@ -132,8 +120,9 @@ class XenCryptUnitTest{
@Test
@DisplayName("should throw an error if passed wrong public key")
fun insertWrongPublicKey() {
val xenditKey = generateXenditKey()
try {
XenCrypt(generatedRsaPublic)
XenCrypt(xenditKey)
} catch (error: WrongPublicKeyError) {
error.message?.contains("Something happen while decoding publicKey")
?.let { assertTrue(it) }
Expand All @@ -143,8 +132,9 @@ class XenCryptUnitTest{
@Test
@DisplayName("should throw an error during decryption if the provided session key is more than 32 bytes")
fun insertWrongPrivateKey() {
val xenditKey = generateXenditKey()
val plain = "test"
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenCrypt = XenCrypt(xenditKey)

val secureRandom = SecureRandom.getInstance("SHA1PRNG")
// Wrong length
Expand All @@ -164,8 +154,9 @@ class XenCryptUnitTest{
@Test
@DisplayName("should not decrypt plain text if the provided during decryption iv is not encoded")
fun insertWrongIv() {
val xenditKey = generateXenditKey()
val plain = "test"
val xenCrypt = XenCrypt(String(RSA_PUBLIC))
val xenCrypt = XenCrypt(xenditKey)

val privateKey = xenCrypt.getSessionKey()
// Generate wrong iv
Expand Down

0 comments on commit 65df688

Please sign in to comment.