Search code examples
javascriptkotlinencryption

How can I get the ciphertext in Kotlin?


Since the website of my school is too difficult to use, I decided to write a mobile app to have a better user expreience last year. I have used Jsoup and other utilities to made it.

I found it's much more difficult to use recently, and there are more autherization pages in the website. I spent a whole day to find out how to translate the code from JS to Kotlin.


Is there a way that I can get the ciphertext of the values or How to make the code to be runnable in the Rhino JavaScript evaluator.

The encryptBase64 code is like this, I don't know how to manage to get the ciphertext in Kotlin

encryptBase64: function (e) {//e=password
    var r = f();
    var t = s.default.MD5(r);
    console.log("r="+r);//r=GN12MVOFWVZJ51KZ
    console.log("t="+t);//r=GN12MVOFWVZJ51KZ
    console.log("l="+l);//l=c2df1fd689074ff84521cea43a677bbf

    var o = s.default.AES.encrypt(r, l, {iv: l, mode: a, padding: d});
    var n = s.default.AES.encrypt(e, t, {iv: t, mode: a, padding: d});
    console.log("o="+o)//o=tAzMY2LKjgC96ZJHXYjorazfL56s//vgFgoerDmaH3g=
    console.log("n="+n)//n=GQNoFJVGL5qdLyb7KxFXAA==
    var o_cipher=o.ciphertext

    var n_cipher=n.ciphertext
    console.log("o_cipher="+o_cipher)o_cipher=b40ccc6362ca8e00bde992475d88e8adacdf2f9eacfffbe0160a1eac399a1f78
    console.log("n_cipher="+n_cipher)//n_cipher=1903681495462f9a9d2f26fb2b115700
    var i = s.default.lib.WordArray.create([].concat(c(o_cipher.words), c(n_cipher.words)));

    console.log("i="+i)//i=b40ccc6362ca8e00bde992475d88e8adacdf2f9eacfffbe0160a1eac399a1f781903681495462f9a9d2f26fb2b115700
    var stringifiedI=s.default.enc.Base64.stringify(i)
    console.log("stringified i="+stringifiedI)//stringified i=tAzMY2LKjgC96ZJHXYjorazfL56s//vgFgoerDmaH3gZA2gUlUYvmp0vJvsrEVcA
    return stringifiedI
}

I thought the ciphertext may be something like AES, not RSA nor MD5, because the server can decrypt it without key (I did not find anything else but the password encrypted).

Ask the school for help is impossible, my school does not approve it.

The Full code is as below (I think it's something like CryptoJS): Full Code in GitHub

What have I tried

  • I have tried to write some code in Kotlin to perform similar function and it does not work appropriately.

I have created an aes encrypt/decrypt extension function like this: aes.kt

And a js.kt that contains callJs()function.

I tried to use the callJs() to get the result.

//fun main
    val a= callJs(JS.encryptCode,"aesOene","encryptBase64","password")
    println(a)
object JS{
    val encryptCode: String by lazy {
        val bytes = this::class.java.getResourceAsStream("/aesOene.js")!!.readBytes()
        String(bytes)
    }
}

And I got this

Exception in thread "main" org.mozilla.javascript.EcmaError: ReferenceError: "window" is not defined. (aesOene#1)

Than I tried to implement the encryptBase64() with this:

// fun main
    val b=encryptBase64("password")
    println(b)

fun aesKeyBuilder():String{
    val p= arrayListOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z")
    val builder=StringBuilder()
    for (t in 0 until 16){
        val o= ceil(35*Math.random()).toInt()
        builder.append(p[o])
    }
    return builder.toString()
}
fun encryptBase64(e:String):String{
    val r=aesKeyBuilder()
    val l="ass-apex".md5
    val t=r.md5
    val o=r.encryptByAES(l,)
    val n=e.encryptByAES(t,)
    val i=o+n
    return i.base64
}

And it does not get the correct result.

What was I expecting

  • A way to get the result of encryptBase64() in Kotlin or Java

Solution

  • For a correct execution, the h.encryptBase64() function requires a WordArray l, which is either set equal to the MD5 hash of the password e using the function h.setSecret(e) or which can alternatively be defined directly as any 16 bytes WordArray, e.g. l = CryptoJS.enc.Utf8.parse('0123456789012345').

    The h.encryptBase64() function then does the following:

    • Via the function f() a random 16 character string r is generated, which consists of a sequence of the characters from p.
    • From r the MD5 hash t is generated.
    • r is encrypted with AES in CBC mode with PKCS#7 padding (ciphertext o_cipher). l is used for both the key and the IV.
    • The plaintext is encrypted with AES in CBC mode with PKCS#7 padding (ciphertext n_cipher). t is used for both the key and the IV.
    • The ciphertexts o_cipher and n_cipher are concatenated (result i).
    • i is Base64 encoded and returned.

    Be aware that the current JavaScript code is unnecessarily complicated and riddled with vulnerabilities (e.g. MD5, Math.Random).
    The usual approach would use a reliable key derivation function (at least PBKDF2) in conjunction with a random, non-secret salt to generate a key that would be used to encrypt the plaintext.
    The salt would be passed to the decrypting side together with the ciphertext so that the key could be reconstructed.
    Actually, it would make sense to revise the JavaScript code before porting, but since you are probably interested in a 1:1 porting of the unchanged code, this is described below:

    • The aesKeyBuilder() function you posted can in principle be used as a counterpart to f(). However, Math.random() should be replaced by SecureRandom#nextDouble(), since Math.random() is not cryptographically secure:

      import java.security.SecureRandom
      ...
      fun aesKeyBuilder(): String {
          val p = arrayListOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z")
          val builder = StringBuilder()
          val secureRandom = SecureRandom()
          for (t in 0 until 16){
              val o= ceil(35 * secureRandom.nextDouble()).toInt()
              builder.append(p[o])
          }
          return builder.toString()
      }
      
    • The next step is to define a function that performs the encryption with AES in CBC mode and PKCS#7 padding, e.g.:

      import javax.crypto.Cipher
      import javax.crypto.spec.IvParameterSpec
      import javax.crypto.spec.SecretKeySpec
      ...
      fun aesEncrypt(key: ByteArray, data: ByteArray): ByteArray {
          val secretKeySpec = SecretKeySpec(key, "AES")
          val ivParameterSpec = IvParameterSpec(key)
          val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
          cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
          return cipher.doFinal(data)
      }
      
    • With this, the logic of the JavaScript code can be ported to Kotlin as follows:

      import android.util.Base64
      import java.security.MessageDigest
      ...
      val password = "my passphrase"
      val plaintext = "The quick brown fox jumps over the lazy dog"
      val key_l = MessageDigest.getInstance("MD5").digest(password.toByteArray())
      val key_r = aesKeyBuilder().toByteArray()
      val key_t = MessageDigest.getInstance("MD5").digest(key_r)
      val encKey_o = aesEncrypt(key_l, key_r)
      val ciphertext_n = aesEncrypt(key_t, plaintext.toByteArray())
      val result_i = encKey_o + ciphertext_n
      val resultB64 = Base64.encodeToString(result_i, Base64.NO_WRAP);
      

    Test:

    For a test, it is best to implement a JavaScript code for decryption that decrypts the ciphertext generated by the JavaScript code. The Kotlin code can be considered compatible if this JavaScript decryption code also decrypts the ciphertext generated by the Kotlin code.

    The following JavaScript code successfully performs such a test:

    var a = CryptoJS.mode.CBC, 
        d = CryptoJS.pad.Pkcs7, l = "", u = "",
        p = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"],
        f = function () { 
            return function (e) {
                for (var r = "", t = 0; t < e; t++) {
                    var o = Math.ceil(35 * Math.random());
                    r += p[o]
                }
                return r
            }(16)
        }, 
        h = { 
            encryptBase64: function (e) {
                var r = f();
                var t = CryptoJS.MD5(r);
                console.log("r="+r);
                console.log("t="+t);
                console.log("l="+l);
                            
                var o = CryptoJS.AES.encrypt(r, l, {iv: l, mode: a, padding: d});
                var n = CryptoJS.AES.encrypt(e, t, {iv: t, mode: a, padding: d});
                console.log("o="+o)
                console.log("n="+n)
                var o_cipher=o.ciphertext
                            
                var n_cipher=n.ciphertext
                console.log("o_cipher="+o_cipher)
                console.log("n_cipher="+n_cipher)
                var i = CryptoJS.lib.WordArray.create([].concat(c(o_cipher.words), c(n_cipher.words)));
                            
                console.log("i="+i)
                var stringifiedI=CryptoJS.enc.Base64.stringify(i)
                console.log("stringified i="+stringifiedI)
                return stringifiedI
            }, 
            setSecret: function (e) {
                u = e, l = CryptoJS.MD5(u)
            }
        };
    
    function c(e) {
        if (Array.isArray(e)) {
            for (var r = 0, t = Array(e.length); r < e.length; r++) t[r] = e[r];
            return t
        }
        return Array.from(e)
    }
          
    // encryption
    h.setSecret('my passphrase') 
    var ciphertext = h.encryptBase64('The quick brown fox jumps over the lazy dog')
    console.log('Ciphertext:', ciphertext)
    
    // decryption from JavaScript ciphertext
    var ciphertextWA = CryptoJS.enc.Base64.parse(ciphertext)
    var encryptedKeyWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(0, 256 / 32))
    var encryptedDataWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(256 / 32))
    var l = CryptoJS.MD5('my passphrase') // apply passphrase from setSecret
    var r = CryptoJS.AES.decrypt({ciphertext: encryptedKeyWA}, l, {iv: l, mode: a, padding: d})
    var t = CryptoJS.MD5(r)
    var e = CryptoJS.AES.decrypt({ciphertext: encryptedDataWA}, t, {iv: t, mode: a, padding: d})
    console.log('Decrypted:', e.toString(CryptoJS.enc.Utf8))
    
    // decryption from Kotlin ciphertext 
    var ciphertextFromKotlin = 'rFXxMQjsIhfCOPiIbgLTJJioVqb7ONCp9Tx3WoTpbF9QxlJNdpR4r6mPtBbnSsEl9PL30hoER6P+RJS9hVUbs/i7ipxhwqiFEbbH97ryZmM='
    var ciphertextWA = CryptoJS.enc.Base64.parse(ciphertextFromKotlin)
    var encryptedKeyWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(0, 256 / 32))
    var encryptedDataWA = CryptoJS.lib.WordArray.create(ciphertextWA.words.slice(256 / 32))
    var l = CryptoJS.MD5('my passphrase') // apply passphrase from Kotlin code
    var r = CryptoJS.AES.decrypt({ciphertext: encryptedKeyWA}, l, {iv: l, mode: a, padding: d})
    var t = CryptoJS.MD5(r)
    var e = CryptoJS.AES.decrypt({ciphertext: encryptedDataWA}, t, {iv: t, mode: a, padding: d})
    console.log('Decrypted:', e.toString(CryptoJS.enc.Utf8))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>