I wrote a dynamic inventory with PHP and a DB. Now I want include some credentials in it. Typically I use ansible-vault and generate the string, put them into the database and only forward the content to the inventory. But sometimes I've credentials (for example from a customer input), that are stored more or less plain in the database. But I don't want to post them plain to the JSON inventory output.
So, PHP needs to generate the ansible-vault result itself.
I looked into the code of ansible vault and I'm sure, Im nearly finished, but I don't understand how Ansible-Vault is converting the binary into the numbers.
<?php
/**
* Add header
*
* @param string $ciphertext the encrypted and hexlified data as a byte string
* @param string $cipher unicode cipher name (for ex, 'AES256')
* @param string $vaultId unicode vault version (for ex, '1.2'). Optional ('1.1' is default)
* @return string a byte str that should be dumped into a file.
*/
function vault_format(string $ciphertext, string $cipher = "AES256", string $vaultId = "1.1") :string {
return "\$ANSIBLE_VAULT;".$vaultId.";".$cipher."\n".$ciphertext;
}
/**
* Python ansible-vault original
def _create_key_cryptography(b_password, b_salt, key_length, iv_length):
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=2 * key_length + iv_length,
salt=b_salt,
iterations=10000,
backend=CRYPTOGRAPHY_BACKEND)
b_derivedkey = kdf.derive(b_password)
return b_derivedkey
*/
function create_key(string $secret, string $salt, int $keyLength, int $iv_length) :string {
return openssl_pbkdf2($secret, $salt, 2* $keyLength+$iv_length, 10000, 'sha256');
// return hash_pbkdf2("sha256", $secret, $salt, 10000, 2* $keyLength + $iv_length);
}
/**
def _gen_key_initctr(cls, b_password, b_salt):
# 16 for AES 128, 32 for AES256
key_length = 32
if HAS_CRYPTOGRAPHY:
# AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes
iv_length = algorithms.AES.block_size // 8
b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)
b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]
elif HAS_PYCRYPTO:
# match the size used for counter.new to avoid extra work
iv_length = 16
b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)
b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])
else:
raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')
b_key1 = b_derivedkey[:key_length]
b_key2 = b_derivedkey[key_length:(key_length * 2)]
return b_key1, b_key2, b_iv
*/
function generate_key_initctr(string $cipher, string $secret, string $salt) :array {
$key = array();
$key_length = 32;
// AES
$iv_length = openssl_cipher_iv_length($cipher);
$derivedkey = create_key($secret,$salt,$key_length,$iv_length);
$iv = substr($derivedkey,$key_length*2,$iv_length);
$key["key1"] = substr($derivedkey,0,$key_length);
$key["key2"] = substr($derivedkey, $key_length,$key_length);
$key["iv"] = $iv;
return $key;
}
/**
b_salt = os.urandom(32)
b_password = secret.bytes
b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)
if HAS_CRYPTOGRAPHY:
b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)
elif HAS_PYCRYPTO:
b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)
else:
raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')
b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])
# Unnecessary but getting rid of it is a backwards incompatible vault
# format change
b_vaulttext = hexlify(b_vaulttext)
return b_vaulttext
*/
function encrypt(string $cipher, string $plaintext, string $secret) :string {
// $salt = openssl_random_pseudo_bytes($ivlen);
$salt = openssl_random_pseudo_bytes(32);
$key = generate_key_initctr($cipher, $secret, $salt);
echo "key: ".bin2hex($key["iv"])."\n";
$ciphertext_raw = openssl_encrypt($plaintext, $cipher, $secret, OPENSSL_RAW_DATA, $key["iv"]);
$hmac = hash_hmac('sha256', $ciphertext_raw, $secret, true);
$salt = bin2hex($salt);
$hmac = bin2hex($hmac);
$crpt = bin2hex($ciphertext_raw);
$vault = $salt."\n".$hmac."\n".$crpt;
$ciphertext = bin2hex( $vault );
return vault_format($ciphertext);
}
/**
def decrypt(cls, b_vaulttext, secret):
b_ciphertext, b_salt, b_crypted_hmac = parse_vaulttext(b_vaulttext)
b_password = secret.bytes
b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)
if HAS_CRYPTOGRAPHY:
b_plaintext = cls._decrypt_cryptography(b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv)
elif HAS_PYCRYPTO:
b_plaintext = cls._decrypt_pycrypto(b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv)
else:
raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in decrypt)')
return b_plaintext
*/
function decrypt(string $cipher, string $vault, string $secret) :string {
$vault_parts = explode("\n",hex2bin($vault));
$salt = hex2bin($vault_parts[0]);
$hmac = hex2bin($vault_parts[1]);
$crpt = hex2bin($vault_parts[2]);
$key = generate_key_initctr($cipher, $secret, $salt);
echo "key: ".bin2hex($key["iv"])."\n";
$plaintext = openssl_decrypt($crpt, $cipher, $secret, OPENSSL_RAW_DATA, $key["iv"]);
return $plaintext;
}
// --------
// $ciphers = openssl_get_cipher_methods();
// print_r($ciphers);
$ciphers = array();
$ciphers[]="aes-256-cbc";
$ciphers[]="aes-256-cbc-hmac-sha1";
$ciphers[]="aes-256-cbc-hmac-sha256";
// $ciphers[]="aes-256-ccm";
$ciphers[]="aes-256-cfb";
$ciphers[]="aes-256-cfb1";
$ciphers[]="aes-256-cfb8";
$ciphers[]="aes-256-ctr";
$ciphers[]="aes-256-ofb";
$ciphers[]="aes-256-xts";
// ansible-vault encrypt_string --vault-password-file test.pass 'hello world'
$plaintext = "hello world";
$secret = "my-secret-password";
$encrypted = "646135396134386432636334623638646362366562653535346133643330616263323033616132333164366134393134353061306235326165343863383264390a653536663236643463633032613437363066313565336332616331646364383730333064353966653436386662323734373864333936386332653835656534310a3433353564373736303038346663396536346330303664616139383632313363";
echo "$plaintext\n";
foreach ($ciphers as $cipher) {
echo "\n\ncipher: $cipher\n";
echo "decrypted:".decrypt($cipher, $encrypted, $secret)."\n";
echo "encrypted:".encrypt($cipher, $plaintext, $secret)."\n";
}
?>
As you can see, I've created first an example and try to decrypt the code.
The output is something like
hello world
cipher: aes-256-cbc
key: f5c682ba666e1007a14d6f0ecdc76388
decrypted:
key: 2ed49bd57d0c93f14889a7f7d21a21c0
encrypted:$ANSIBLE_VAULT;1.1;AES256
643366363136336363313864386630323566343665326465346637333135646336633339376638383031396234343934356638336564623165343835633238620a303332306430376263343365386533343234373362323638336335326232656665343639383437623066636637356637633766616463303738383863363730370a3364633134303264666231306237353032356435393761313561643733623564
cipher: aes-256-cbc-hmac-sha1
key: f5c682ba666e1007a14d6f0ecdc76388
decrypted: <binary-output>
key: ecf4c8215533e8fd672a67314d8c7ed0
encrypted:$ANSIBLE_VAULT;1.1;AES256
303433383036656133383832376234353237666332313937323263363966633265643936303535613837353932663661653035396534616463376331356539640a393939643935653163626364653434626266373062656534656335323066343634343266643665326261633134373636316239653561666162366161356536610a6538333536356236643165613462386564363635373464353733376631626565
cipher: aes-256-cbc-hmac-sha256
key: f5c682ba666e1007a14d6f0ecdc76388
decrypted: <binary-output>
key: 0981caf5fb38c63791091d10fc7ea5a6
encrypted:$ANSIBLE_VAULT;1.1;AES256
323435623935626264623363633434333132333763333162386431386339373365623064393566663262326435363036623461666630623136323832336562620a353531306434346434316435326639343262343362633261363230363661666535363861663039336132373264366336396139363538636138353337663164390a6464306430636434366663316265353536636233663538323862343438373563
cipher: aes-256-cfb
key: f5c682ba666e1007a14d6f0ecdc76388
decrypted: <binary-output>
key: 15eca76feb2bfff3341b683928a84557
encrypted:$ANSIBLE_VAULT;1.1;AES256
366634656434663732343761643038393233623736616235336163313161366539353766623637353761366135303031396438623733663862303566623438390a373339613366383539396161383937663838306262333633303765333833623561313132363536616137353664333238643664393833333662303231623363340a34333930313464396133316130346633313531306334
So I would expect that the correct algoritm shows me directly in PHP, when it was able to decrypt the string. Also I wrote a small playbook just to test the PHP result.
But when I try it with an example playbook like:
- name: My Test
hosts: localhost
gather_facts: false
vars:
test: !vault |
$ANSIBLE_VAULT;1.1;AES256
3566383238386333633437...24c49c24d60
tasks:
- debug:
msg: "{{ test }}"
The following error occures:
PLAY [My Test] **********************************************************************************************************************************************************************************************************************
TASK [debug] ************************************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! =>
msg: Decryption failed (no vault secrets were found that could decrypt)
PLAY RECAP **************************************************************************************************************************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
With -vvvvvvv
I'm getting
Tried to use the vault secret (default) to decrypt (None) but it failed. Error: HMAC verification failed: Signature did not match digest.
So, neither decrypting nor encrypting is really working. I'm not really sure, what cipher method is used (that's the reason for the loop over all AES256 cipher). Also is there a pendant for PBKDF2HMAC in PHP (tried openssl_pbkdf2
and hash_pbkdf2
, I think, there is also a problem with the correct key? And I've problems to understand the Python code. Can the Python code
b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]
be replaced in PHP with?
substr($derivedkey,$key_length * 2, $iv_length);
And maybe the last problem is the "unused key1 and key2. The original Python code contains this encrypt:
@staticmethod
def _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):
cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())
b_ciphertext += encryptor.finalize()
I replaced it with:
openssl_encrypt($plaintext, $cipher, $secret, OPENSSL_RAW_DATA, $key["iv"]);
The Cipher
is unknown to me? key1 is some parts of the generated key from PBKDF2HMAC. That sounds strange to me. What does it mean?
My Answer:
To adapt Ansible-Vault functionality in PHP, you can use the `openssl_encrypt` function to encrypt the credentials before storing them in the database. Here is an example of how you can achieve this:
php
// Credentials to be encrypted
$credentials = 'my_secret_password';
// Encryption key (should be securely stored)
$key = 'my_secret_key';
// Encrypt the credentials using AES-256-CBC encryption
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$encrypted = openssl_encrypt($credentials, 'aes-256-cbc', $key, 0, $iv);
// Store the encrypted credentials and IV in the database
// Make sure to store $encrypted and $iv in separate columns
// Output the encrypted credentials in ansible-vault format
echo "!\n";
echo base64_encode($iv) . "\n";
echo base64_encode($encrypted) . "\n";
This code snippet encrypts the credentials using AES-256-CBC encryption and outputs them in ansible-vault format. You can then store the encrypted credentials and IV in the database and retrieve them when needed.
When decrypting the credentials in your PHP script, you can use the `openssl_decrypt` function with the stored IV and encryption key to decrypt the credentials.
Remember to securely store the encryption key and follow best practices for handling sensitive information.
Rate this post
4 of 5 based on 3134 votesComments