0

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?

β.εηοιτ.βε
  • 33,893
  • 13
  • 69
  • 83
TRW
  • 876
  • 7
  • 23
  • 1
    FWIIW, decrypt seems to be something this person have achieved: https://github.com/daniel-ness/ansible-vault – β.εηοιτ.βε Feb 02 '21 at 20:43
  • 1
    You shouldn't store customer values plain in a database. Also, I'm reading it as if you're trying to mimic the ansible-vault internal encryption method. I don't think that's a wise choice, what if Ansible updates this method? – Kevin C Feb 02 '21 at 23:02
  • @β.εηοιτ.βε - you're my hero. Thanks. This library is absolutly working in both directions. I checked the code and "only" missed two thinks - used the wrong keys for encrypting and did not understand, what the Padding is doing. Everything else is correct. – TRW Feb 03 '21 at 11:08
  • @KevinC - I don't store customer data plain in the database. The problem is - I need to expose that data to an Ansible inventory (as JSON) and that is currently "plain" (so I need to decrypt it from the database and put it into the JSON output). I'd like to avoid that. Because Ansible uses Ansible Vault, this is the only "good" solution - or you need to implement your own decrypting filter in Ansible. – TRW Feb 03 '21 at 11:13
  • And - I'm not interested in the internal of Ansible Vault. I need to find a way to generate the code - so I "reverse engineered" it. If Ansible changes the output (like in 1.2 or later in 2.0) then - of course there is another algorithm. But 1.1 will still work till somebody (or me) create a PHP equivalent. – TRW Feb 03 '21 at 11:15
  • Also - one funny thing. I tried the playbook within my playbooks directory with a ansible.cfg configured inventory file. Because for tests I used a different vault and then (even with a correct content) the playbook here fails because the vault password in the inventory differ from the vault password in the example. Using vault-id would help...haha – TRW Feb 03 '21 at 11:20

0 Answers0