diff --git a/src/Utility.php b/src/Utility.php index 9f3a527..820852f 100644 --- a/src/Utility.php +++ b/src/Utility.php @@ -78,28 +78,26 @@ public function generateOnboardingSignature($data, $secret){ private function encrypt($dataToEncrypt, $secret) { try { - // Use the first 16 bytes of the secret as the key $key = substr($secret, 0, 16); - - // Use the first 12 bytes of the key as IV - $iv = substr($key, 0, 12); - - // Encrypt the data using AES-128-GCM + + // Generate a fresh random 12-byte nonce per call (fixes AES-GCM nonce reuse). + // A static IV derived from the key allows keystream recovery and tag forgery + // (NIST SP 800-38D §8.3 Forbidden Attack) using only two captured ciphertexts. + $iv = random_bytes(12); + $cipher = 'aes-128-gcm'; - $tag = ''; // Authentication tag will be filled after encryption + $tag = ''; $encryptedData = openssl_encrypt($dataToEncrypt, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16); - + if ($encryptedData === false) { - throw new Exception('Encryption failed'); + throw new \Exception('Encryption failed'); } - - // Concatenate encrypted data with the authentication tag - $finalData = $encryptedData . $tag; - - // Convert to hex string - return bin2hex($finalData); - } catch (Exception $e) { - throw new Exception('Encryption failed: ' . $e->getMessage()); + + // Output format: iv (12 bytes) || ciphertext || tag (16 bytes), hex-encoded. + // Receiver must read the first 24 hex chars as the IV before decrypting. + return bin2hex($iv . $encryptedData . $tag); + } catch (\Exception $e) { + throw new \Exception('Encryption failed: ' . $e->getMessage()); } } diff --git a/tests/OAuthTokenClientTest.php b/tests/OAuthTokenClientTest.php index aeeeeda..cceb3b3 100644 --- a/tests/OAuthTokenClientTest.php +++ b/tests/OAuthTokenClientTest.php @@ -118,15 +118,21 @@ public function testRevokeTokenExecutesValidationFailure(){ } public function testGenerateOnboardingSignature(){ - // encrypted from Java sdk; - $expectedSignature = "37ad80c568a44f6999aa8f80bb5080dbc50eed353d325cb94d624bf82a9a36d12e4fd00490bc06271e06628c889c6b1c2a48e2f355f8598210d1b1c8c1c42dfcd02502f1515294028fd4"; $api = new Api("key", "secret"); $secret = "mzhK9zRdA2QoLxhlSR6Pg721"; $attributes = [ "submerchant_id" => "avaBWdazt7LoYu", - "timestamp" => 1741098479 + "timestamp" => 1741098479 ]; - $actualSignature = $api->utility->generateOnboardingSignature($attributes, $secret); - $this->assertEquals($expectedSignature, $actualSignature); + + $sig1 = $api->utility->generateOnboardingSignature($attributes, $secret); + $sig2 = $api->utility->generateOnboardingSignature($attributes, $secret); + + // Output is hex: 12-byte IV + ciphertext + 16-byte tag — minimum 28 bytes = 56 hex chars. + $this->assertMatchesRegularExpression('/^[0-9a-f]+$/', $sig1); + $this->assertGreaterThanOrEqual(56, strlen($sig1)); + + // Each call must produce a unique ciphertext (random nonce per call). + $this->assertNotEquals($sig1, $sig2); } }