Login through Mobile Server using PHP

Hi,

I am trying to login on mobile server through PHP language. I was able to get the server public key using this code (sending my public key on Connect method). This is working:

private const DH_PRIME = 'F488FD584E49DBCD20B49DE49107366B336C380D451D0F7C88B31C7C5B2D8EF6F3C923C043F0A55B188D8EBB558CB85D38D334FD7C175743A31D186CDE33212CB52AFF3CE1B1294018118D7C84A70A72D686C40319C807297ACA950CD9969FABD00A509B0246D3083D66A45D419F9C7CBD894B221926BAABA25EC355E92F78C7';
private const DH_GENERATOR = '02';
 
$privateKey = gmp_init(uniqid(), 32);
$publicKey = gmp_powm(self::DH_GENERATOR, $privateKey, base_convert( self::DH_PRIME, 16,10));

Then, I understand that I must compute the server public key with my private key using the PRIME value to get the shared key and then encrypt the username and password (this is not working):

// Decode server public returned key (encoded in base 64)
$serverPKBase64Decoded = base64_decode($apiResponse['response']['Command']['OutputParams']['Param']['7']['@attributes']['Value']);
$serverPublicKey16 = bin2hex($serverPKBase64Decoded);
 
// Calculate shared key
$prime = gmp_init(self::DH_PRIME,16);
$serverPk = gmp_init($serverPublicKey, 16);
$localPk = gmp_init($localPrivateKey,32);
$sharedKey = gmp_powm($serverPk, $localPk, $prime);
 
// Get shared key as hexadecimal value
$sharedKeyHex = gmp_strval($sharedKey, 16);

Now, I has following your JS samples to know what is required to get from shared key to have the key and the initialization vector to make the encryption, but I always get error code 16 with text “Incorrect public key”.

// I need an IV of 16 bytes and key with 32 bytes.
$iv = substr($sharedKeyHex,0,16);
$key = substr($sharedKeyHex,16, 32);
 
$options = OPENSSL_RAW_DATA;
 
$encryptedUsername = base64_encode(openssl_encrypt($username,$cipher,$key,$options,$iv));
$encryptedPassword = base64_encode(openssl_encrypt($password,$cipher,$key,$options,$iv));

I don’t know what is wrong here. I have tried with multiple possibilities, but it is not working yet. Please, could you help me or explain the detailed process here?

We cannot use JS for creating bookmarks because we need to execute this code on server side, not client side.

Thank you very much

Hi,

One thing that you may try first is to make an unit test on your DH component - two instances exchanging keys do they generate same shared secrets?

Once this is fine, make sure you call the Connect command with the right parameters for :

EncryptionPadding and PrimeLength.

Than once public keys exchanged and shared key calculated , for the encryption it should be used AES with respective padding.

Hope this helps,

Svetlana

P.S. we do have a .net sdk that may be used for backend functionality if it is a possibility for you project.

Hi Joaquin,

Just to mention, that if your Mobile Server is running HTTPS, you could enable plain text authentication, without sacrificing your security.

If that’s the case, with a simple setting in the server’s XML file, you will be able to pass user credentials, without encrypting them (resp providing public key).

Again, this should be used only when Mobile server is running on HTTPS or for limited debugging purposes (when on HTTP).

Hi, Thank you for your answers. I have done unit testing with my DH component and it is working well. I have already tested to encrypt the data using PHP and decrypt with C#/.NET. It is working well with the same IV and Key, so the issue may be while getting the bytes from the shared key for obtaining the key and the initialization vector.

Please, could you clarify how to get the IV and Key from the shared key? It it not clear here:

https://doc.developer.milestonesys.com/mipsdkmobile/reference/protocols/mobile_logon.html

It is not an option for us to run the applications with HTTPS.

Thanks

Hi,

For the AES encryption, we take first 16 byte for IV and next 32 bytes for key , set CipherMode to CBC,

and padding to the agreed one,

Some things that we have encountered while making our implementation and may help you:

  • The random you generate should be a positive number
  • Due to different realization of big integers in different libs. , you may need to reverse the byte array of the Mobile Server public key .

Thanks for your answer but I still not able to get it working. Please, find the code I am using to known if any step is wrong:

1- Base64 decode public key (I got binary data):

$serverPKBase64Decoded = base64_decode($apiResponse['response']['Command']['OutputParams']['Param']['7']['@attributes']['Value']);

2- Reverse Binary Data:

$serverPublicKeyReversed = $this->reverseBinaryData($serverPublicKey);
 
private function reverseBinaryData(string $serverPKBase64Decoded): string
    {
        $reversed = "";
        try {
            for($i = strlen($serverPKBase64Decoded) -1; $i > 0 ; $i--){
                $reversed .= $serverPKBase64Decoded[$i];
            }
        } catch (Throwable $throwable) {
            $this->logger->critical("Reverse Binary Data: Ko. Exception details: {$throwable->getMessage()}.");
        }
        return $reversed;
    }

3- Get Server Public Key reversed as hexadecimal:

$serverPublicKeyHex = (unpack("H*", $serverPublicKeyReversed))[1];

4- Calculate shared key:

// Calculate shared key
$prime = gmp_init(self::DH_PRIME, 16);
$serverPk = gmp_init($serverPublicKeyHex, 16);
$privateKey = gmp_init($localPrivateKey, 32);
$sharedKey = gmp_powm($serverPk, $privateKey, $prime);
// Get shared key as hexadecimal value (it must have a length of 256)
$sharedKeyHex = gmp_strval($sharedKey, 16);

5- Get IV and Key:

// Proceed to get key and iv
// IV: first 16 byte
// KEY: next 32 bytes
 
$iv_size = openssl_cipher_iv_length(self::CIPHER);
$iv = mb_strcut($sharedKeyHex, 0, $iv_size);
$key = mb_strcut($sharedKeyHex, 16, 32);

6- Encrypt username and password

$usernameEncrypted = openssl_encrypt(self::USERNAME, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
$passwordEncrypted = openssl_encrypt(self::PASSWORD, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);

7- Base64 encode of encrypted data:

$usernameBase64 = base64_encode($usernameEncrypted);
$passwordBase64 = base64_encode($passwordEncrypted);

I got values that seem to be base64 encoded values:

$usernameBase64 ~ rQT/BYO8+w+mpZc27nHFFw==

$passwordBase64 ~ 9B8hDXWkhxfLmM6Lyb1c/g==

8- Send parameters to Mobile Server:

params = [
                'SupportsAudioIn' => 'Yes',
                'SupportsAudioOut' => 'Yes',
                'Username' => $usernameBase64,
                'Password' => $passwordBase64,
                'LoginType' => 'Base',
                'NumChallenges' => 100,
                'SupportsResampling' => 'Yes',
                'SupportsExtendedResamplingFactor' => 'Yes',
                'ClientType' => 'WebClient'
            ];
 
            $xmlMessage = $this->createXMLMessage('LogIn', $this->sequenceId, $params, $this->connectionId);
 
            if (!$this->apiInitialized) {
                $this->apiInitialized = $this->initializeMilestoneApiClient();
            }
 
            if ($this->apiInitialized && $xmlMessage !== '') {
 
                $apiResponse = $this->apiClient->getPostResource(':8081/XProtectMobile/Communication', 0, 'Connection', $xmlMessage, 'text/xml');
 
                if (isset($apiResponse)) {
                    $success = true;
                }
 
            }

Always, I got this response:

Please, could you check what is wrong?

Thanks.

Hi,

I am sorry to hear that.

Few more thoughts:

  • In regards of reversing - it might be needed or not

    • if it is needed
      • Use Prime
      • Reverse Server key
    • If not
      • Do reverse Prime and not the key
    • Just to make sure - try the four options - ((Non)Reversed Prime and (non)Reversed Server key)
  • Reversing

    • First convert to byte array and then reverse (or reverse 2 by 2 symbols)
  • What is the value of CIPHER you set?

  • I don’t see Padding PKCS7 setup (don’t know what is the default one)

  • In our internal implementation we do put a zero byte for the big numbers to make sure the numbers are interpreted correctly as positive

P.S. setting certificate for Mobile Server, may reduce your trouble a lot .

Hi Svetlana,

We have made more tries to get it working, but it still fails. Please, could you tell us if there is a manner (for debugging only) to get your calculated SHARED KEY (logs for example) to check if it matchs with our SHARED KEY. Or better, could you send us your code where you generate the shared key to make some test in a dummy application (as a mock server)?

We need to solve it without needing a HTTPS Certificate. It is not a good solution for us.

Thank you very much. Best regards

Hi again,

Finally, I got it. It is required to reverse the shared key too on PHP language.

The combination is (it may help other users):

  1. Generate the own public key, converting it to byte array, reverse the array and adding a 0 on the end. Then, encode to base64 to send on “Connect” command.
  2. Decode from base64 the server public key (Milestone Mobile Server), convert to byte array and reverse. Then convert to HEX string.
  3. Convert the server public key from HEX string to BigInteger.
  4. Calculate SHARED KEY using the server public key and own private key using OpenSSL or PHPSECLIB (got the same result with both methods as binary string).
  5. Reverse binary string with the SHARED KEY.
  6. Get IV (first 16 bytes) and KEY (next 32 bytes):
$key = substr($sharedKeyBinaryReversed, 16, 32);
$iv = substr($sharedKeyBinaryReversed, 0, 16);
  1. (7.) Encrypt password (with PHPSECLIB or OpenSSL same results):
$cipher = new AES('cbc');
                $cipher->setKeyLength(256);
                $cipher->setIV($iv);
                $cipher->setKey($key);
 
                $usernameEncrypted = base64_encode($cipher->encrypt(self::USERNAME));
                $passwordEncrypted = base64_encode($cipher->encrypt(self::PASSWORD));
 
                // It works as lines above but with Open SSL
private const CIPHER = 'AES-256-CBC';
                /*$padding = 0;
                $usernameEncrypted = openssl_encrypt(self::USERNAME, self::CIPHER, $key, $padding, $iv);
                $passwordEncrypted = openssl_encrypt(self::PASSWORD, self::CIPHER, $key, $padding, $iv);*/

This is my “params” array sent to Milestone:

$params = [
                    'SupportsAudioIn' => 'Yes',
                    'SupportsAudioOut' => 'Yes',
                    'Username' => $usernameEncrypted,
                    'Password' => $passwordEncrypted,
                    'LoginType' => 'Basic',
                    'NumChallenges' => 100,
                    'SupportsResampling' => 'Yes',
                    'SupportsExtendedResamplingFactor' => 'Yes',
                    'ClientType' => 'WebClient'
                ];

Thank you for your support. BR

Great to hear that, Joaquin!

Thank you for sharing your solution!

Well done, Joaquin !

Great work !