Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions src/Utility.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down
16 changes: 11 additions & 5 deletions tests/OAuthTokenClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading