I am trying to decrypt a response Tokens which where encrypted using a Diffie-Hellman (type curve25519) via AES-GCM and additional authenticated data (AAD). I am new to PHP so I am not sure how can this be accomplished.
here is my attempt:
<?php
$private_key = openssl_pkey_get_private("file:///vault-sink/private");
$pub_key_struct = file_get_contents("/vault-sink/public");
$json_public_key = json_decode($pub_key_struct);
$secret = openssl_dh_compute_key($json_public_key->curve25519_public_key, $private_key);
$token_response = file_get_contents("/vault-sink/token.txt");
$token_response_json = json_decode($token_response);
echo $token_response_json->encrypted_payload;
openssl_decrypt(
$secret,
$token_response_json->encrypted_payload,
$token_response_json->nonce,
$aad = "test"
);
error
PHP Fatal error: Uncaught TypeError: openssl_dh_compute_key(): Argument #2 ($private_key) must be of type OpenSSLAsymmetricKey, bool given in /vault-sink/php-client/main.php:10
content of public and private keys:
# cat public
{"curve25519_public_key":"ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs="}
# cat private
9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6
So my guess is that the private's key format is not readable by openssl_pkey_get_private
function. My question is how can I use these private and public keys in openssl_dh_compute_key
?
For more context, below is an example on how I did it with go.
package main
import (
"encoding/json"
"fmt"
"log"
"os"
dh "github.com/hashicorp/vault/helper/dhutil"
)
func check(e error) {
if e != nil {
log.Fatal(e)
}
}
func generatePublicPrivateKey() {
var keyinfo dh.PublicKeyInfo
public, private, err := dh.GeneratePublicPrivateKey()
check(err)
keyinfo.Curve25519PublicKey = public
pubkey, err := json.Marshal(keyinfo)
check(err)
err = os.WriteFile("public", []byte(pubkey), 0644)
if err != nil {
log.Fatal(err)
}
privateKey := fmt.Sprintf("%x", private)
err = os.WriteFile("private", []byte(privateKey), 0644)
if err != nil {
log.Fatal(err)
}
}
func main() {
generatePublicPrivateKey()
}
output
# cat public
{"curve25519_public_key":"ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs="}
# cat private
9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6
then I run Vault Agent and get the following response ("token.txt"):
{
"curve25519_public_key": "ONwU5iknsVP57p7PdHtN4rzxbivMB4Bt5o2BJFC6oSc=",
"nonce": "AA2VibL5SXE9MU19",
"encrypted_payload": "pPseQIue1IXxutEKoZrjsORj+8AZVgRrcTRvaPVxukzU2w28TL4T0be7aGFKZmHwudiHiNQyp5i8D0ZUgP/ILLYPfhO+gwxUFEDhA4PJNAgKc8nSaMpjG9RipAyRcepgk42SXuRIgZ1D7HrmeWT8"
}
for more info, check https://www.vaultproject.io/docs/agent/autoauth#encrypting-tokens
and then I decrypt the response as following
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
dh "github.com/hashicorp/vault/helper/dhutil"
)
func check(e error) {
if e != nil {
log.Fatal(e)
}
}
func main() {
var envelope dh.Envelope
var keyinfo dh.PublicKeyInfo
token_env, err := os.ReadFile("token.txt")
check(err)
err = json.Unmarshal(token_env, &envelope)
check(err)
ourPublic, err := os.ReadFile("public")
check(err)
ourPrivate, err := os.ReadFile("private")
check(err)
json.Unmarshal(ourPublic, &keyinfo)
privatekey, err := hex.DecodeString(string(ourPrivate))
check(err)
secret, err := dh.GenerateSharedSecret(privatekey, envelope.Curve25519PublicKey)
check(err)
shared_key, err := dh.DeriveSharedKey(secret, keyinfo.Curve25519PublicKey, envelope.Curve25519PublicKey)
check(err)
test, err := dh.DecryptAES(shared_key, envelope.EncryptedPayload, envelope.Nonce, []byte("test"))
check(err)
fmt.Println(string(test))
}
output
hvs.CAESIB1eP79cHXPc4ttZIz20qL2I2A8kcfNpkhPjvIE4DZt3Gh4KHGh2cy4wNWV1WHB5NWNuZXV1dDMzdkh4YkY2OUE
The shared secret can be determined in PHP with the Sodium library and the sodium_crypto_scalarmult()
function.
For the calculation of the shared key the Go source code of DeriveSharedKey()
shows that the shared key is derived with HKDF (see hkdf.go) and the two public keys are sorted and used as salt and info. For this purpose hash_hkdf()
can be used on the PHP side.
Finally, decryption takes place with AES-256 in GCM mode, e.g. with openssl_encrypt()
. In contrast to the Go code, where ciphertext and tag are concatenated, openssl_encrypt()
handles both separately.
The resulting PHP code is (for simplicity, the input data is assigned directly, i.e. without file I/O):
// Input data
$payload = base64_decode("pPseQIue1IXxutEKoZrjsORj+8AZVgRrcTRvaPVxukzU2w28TL4T0be7aGFKZmHwudiHiNQyp5i8D0ZUgP/ILLYPfhO+gwxUFEDhA4PJNAgKc8nSaMpjG9RipAyRcepgk42SXuRIgZ1D7HrmeWT8");
$nonce = base64_decode("AA2VibL5SXE9MU19");
$ourPublicKey = base64_decode("ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs=");
$theirPublicKey = base64_decode("ONwU5iknsVP57p7PdHtN4rzxbivMB4Bt5o2BJFC6oSc=");
$privateKey = hex2bin("9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6");
// Get shared secret
$sharedSecret = sodium_crypto_scalarmult($privateKey, $theirPublicKey);
print("Shared secret (hex): " . sodium_bin2hex($sharedSecret) . PHP_EOL); // Shared secret (hex): 48cb642fe6ecd7e4ff4a58610524f873e8ab86b8ccb195f0c90d59c477d6f437
// Get shared key
// Go: hkdf.New(hash, secret, salt, info)
// PHP: hash_hkdf($algo, $key, $length, $info, $salt)
if (strcmp($ourPublicKey, $theirPublicKey) == -1) {
$salt = $ourPublicKey;
$info = $theirPublicKey;
} else { // for simplicity the 0 case is not considered dedicated, the Go code throws an exception here
$salt = $theirPublicKey;
$info = $ourPublicKey;
}
$sharedKey = hash_hkdf("sha256", $sharedSecret, 32, $info, $salt);
print("Shared key (hex): " . bin2hex($sharedKey) . PHP_EOL); // Shared key (hex): cacbda4874426b1208903d24378bdc0fb7a7dd08c91b5f275c11ff39e58add38
// Decrypt with AES-256, GCM
$ciphertext = substr($payload, 0, strlen($payload) - 16);
$tag = substr($payload, strlen($payload) - 16, 16);
$aad = "test";
$plaintext = openssl_encrypt($ciphertext, "aes-256-gcm", $sharedKey, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
print("Plaintext: " . $plaintext . PHP_EOL); // Plaintext: hvs.CAESIB1eP79cHXPc4ttZIz20qL2I2A8kcfNpkhPjvIE4DZt3Gh4KHGh2cy4wNWV1WHB5NWNuZXV1dDMzdkh4YkY2OUE
with the same output as the Go code.