Search code examples
phpkotlinwsseorocrm

Access to ORO-Platform API with Koltin fails while generating WSSE-Header


Given to this PHP-file for generating the WSSE-Header to access the ORO-Platform API (https://oroinc.com/oroplatform/). Official documentation for building the WSSE-Header: https://oroinc.com/orocrm/doc/2.3/dev-guide/cookbook/how-to-use-wsse-authentication#how-to-use-wsse-authentication

<?php

$userName = 'admin';
$userApiKey = 'dfeeb001cc4b947d8790c906d196d78a41915749';

$nonce = base64_encode(substr(md5(uniqid()), 0, 16));
$created  = date('c');

// Just for test for StakeOverFlow
created = "2019-02-05T14:45:43+01:00"
nonce = "REBxdWVYkSrAADwORS+H5w=="

$digest   = base64_encode(sha1(base64_decode($nonce) . $created . $userApiKey, true));

$wsseHeader = "Authorization: WSSE profile=\"UsernameToken\"\n";
$wsseHeader.= sprintf(
    'X-WSSE: UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
    $userName,
    $digest,
    $nonce,
    $created
);

echo $wsseHeader;

I need the same logic in Kotlin for an Android App.

Here are so far my Code

package de.warehouse.prozessio.appwarehouse.manager

import android.util.Base64
import java.util.*
import android.util.Log
import java.security.MessageDigest
import java.text.SimpleDateFormat

class OroAPIManager {
    fun getHeader() {
        val md = MessageDigest.getInstance("SHA-1")

        val user = "admin"
        val key = "dfeeb001cc4b947d8790c906d196d78a41915749"
        var nonce = String(Base64.encode(md.digest(UUID.randomUUID().toString().toByteArray()), 0, 16,0)).dropLast(1)
        var created = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.GERMAN).format(Date())
        created = created.replaceRange(created.length - 2, created.length - 1, ":0")
        // Just for test for StackOverFlow
        created = "2019-02-06T15:37:11+01:00"
        nonce = "NjA3Yzc4ZjEwY2ZjNTY4NA=="

        val digest = Base64.encode((convertToSha1String(String(Base64.decode(nonce.toByteArray(), Base64.DEFAULT)) + created + key)).toByteArray(), Base64.DEFAULT)
        var wsseHeader = "Authorization: WSSE profile=\"UsernameToken\"\n";
        wsseHeader += "X-WSSE: UsernameToken Username=\"$user\", "
        wsseHeader += "PasswordDigest=\"${String(digest).dropLast(1)}\", "
        wsseHeader += "Nonce=\"$nonce\", "
        wsseHeader += "Created=\"$created\""

        Log.i("data", wsseHeader)
    }

    private fun convertToSha1String(raw: String): String {
        var convertedString = "";
        val bytes = raw.toByteArray()
        val md = MessageDigest.getInstance("SHA-1")
        val digest = md.digest(bytes)
        for (byte in digest) convertedString += "Cn".format(byte)
//        for (byte in digest) convertedString += byte.toChar()
//        for (byte in digest) convertedString += byte.toString()
        Log.i("data", "Converted String: $convertedString")

        return convertedString
    }
}

The result for the PHP script are:

X-WSSE: UsernameToken Username="admin", PasswordDigest="fukhjBVWYl0i08iPaLBeXTJkT4U=", Nonce="NjA3Yzc4ZjEwY2ZjNTY4NA==", Created="2019-02-06T15:37:11+01:00"

And this is correct I have access to Oro.

The result for Kotlin is:

X-WSSE: UsernameToken Username="admin", PasswordDigest="Q25DbkNuQ25DbkNuQ25DbkNuQ25DbkNuQ25DbkNuQ25DbkNuQ25Dbg==", Nonce="NjA3Yzc4ZjEwY2ZjNTY4NA==", Created="2019-02-06T15:37:11+01:00"

Which is incorrect. The problem I guess is the raw-Flag for PHP-sha1 function. As I try several things to convert the Bytearray to a raw string but without any success.

Here are the thread in the Oro-Forum: https://forum.oroinc.com/oro-platform/how-do-i-questions/topic/generate-api-wss-header-for-kotlin#post-37936. They are very friendly there, but could not give help this time.

Got anyone a clue, how to bring the two different results together ?

EDIT:

Thanks to @Michael Bessolov for pointing me into the right direction. Here are the complete solution to grant access from Android the the ORO-API:

import android.content.Context
import android.preference.PreferenceManager
import android.util.Base64
import java.util.*
import java.nio.charset.Charset
import java.security.MessageDigest
import java.text.SimpleDateFormat

class OroAPIManager constructor(val context: Context) {
    fun getHeader(): String {
        val prefs = PreferenceManager.getDefaultSharedPreferences(this.context)
        val user = prefs.getString("oro_user", "<unset>")
        val key = prefs.getString("oro_api_key", "<unset>")

        val uid = UUID.randomUUID().toString();
        val nonce = String(Base64.encode(convertToDigestString(uid, "MD5").toByteArray(), 0, 16,0)).dropLast(1)
        var created = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.GERMANY).format(Date())
        created = created.replaceRange(created.length - 2, created.length - 1, ":0")

        val nonceBase64 = String(Base64.decode(nonce.toByteArray(), Base64.DEFAULT))
        val sha1 = hexToBin(convertToDigestString(nonceBase64  + created + key, "SHA-1"))
        val digest = Base64.encode(sha1.toByteArray(Charset.forName("ISO-8859-1")), Base64.DEFAULT)

        var wsseHeader = "Authorization: WSSE profile=\"UsernameToken\"\n";
        wsseHeader += "X-WSSE: UsernameToken Username=\"$user\", "
        wsseHeader += "PasswordDigest=\"${String(digest).dropLast(1)}\", "
        wsseHeader += "Nonce=\"$nonce\", "
        wsseHeader += "Created=\"$created\""

        return wsseHeader;
    }

    private fun convertToDigestString(raw: String, digestName: String): String {
        val bytes = raw.toByteArray()
        val md = MessageDigest.getInstance(digestName)
        val digest = md.digest(bytes)

        val hexChars = "0123456789abcdef"
        val convertedString = StringBuilder(digest.size * 2)

        digest.forEach {
            val i = it.toInt()
            convertedString.append(hexChars[i shr 4 and 0x0f])
            convertedString.append(hexChars[i and 0x0f])
        }

        return convertedString.toString()
    }

    private fun hexToBin(hexStr: String): String {
        val output = StringBuilder("")
        var i = 0
        while (i < hexStr.length) {
            val str = hexStr.substring(i, i + 2)
            output.append(Integer.parseInt(str, 16).toChar())
            i += 2
        }

        return output.toString()
    }
}

Solution

  • I'm not familiar with Kotlin at all, but convertedString += "Cn".format(byte) looks suspicious in today's world of multi-byte encodings. Would the following piece (inspired by https://www.samclarke.com/kotlin-hash-strings/) produce a different result?

    val HEX_CHARS = "0123456789ABCDEF"
    val convertedString = StringBuilder(digest.size * 2)
    
    digest.forEach {
        val i = it.toInt()
        convertedString.append(HEX_CHARS[i shr 4 and 0x0f])
        convertedString.append(HEX_CHARS[i and 0x0f])
    }