module

Krikri::Vault

Vault - Ansible Vault (AES256) encrypt/decrypt.

File format (verified against real ansible-vault, not assumed from memory): $ANSIBLE_VAULT;1.1;AES256 <hex, wrapped at 80 columns>

Where the wrapped hex, once unwrapped and hex-decoded, is itself ASCII text: "<hex(salt)>\n<hex(hmac)>\n<hex(ciphertext)>". That's a deliberate double hex-encoding, not a bug - Ansible's own format does this. salt is 32 random bytes; a single PBKDF2-HMAC-SHA256 call over (password, salt, 10_000 iterations, 80-byte output) is split into a 32-byte AES key, a 32-byte HMAC key, and a 16-byte CTR IV; the plaintext is PKCS7-padded to a 16-byte boundary before AES-256-CTR encryption; the HMAC is computed over the ciphertext (encrypt-then-MAC) using the derived HMAC key.

Constants

BLOCK_SIZE = 16
DERIVED_SIZE = 80
HEADER_PREFIX = "$ANSIBLE_VAULT;"
PBKDF2_ROUNDS = 10000
SALT_SIZE = 32
WRAP_COLUMN = 80

Class methods

add_vault_id(label : String, secret : String) : Nil
Source
candidate_passwords(content : String) : Array(String)

Every password worth trying for content, best candidate first.

Source
decrypt(content : String, password : String) : String
Source
encrypt(plaintext : String, password : String) : String
Source
encrypted?(content : String) : Bool

True if content looks like an Ansible Vault-armored file/string (starts with the "$ANSIBLE_VAULT;" header), independent of whether its cipher/version are ones we actually support.

Source
maybe_decrypt(content : String) : String

Decrypts content if it's vault-encrypted (using the configured password), otherwise returns it unchanged. This is what every file-loading call site should call instead of using the raw file content directly.

Source
maybe_decrypt_json(value : JSON::Any) : JSON::Any

Like maybe_decrypt, but for a parsed variable value rather than a raw file - covers Ansible's other vault use case: a single value inline in an otherwise-plaintext vars file, tagged !vault (what ansible-vault encrypt_string produces, e.g. db_password: !vault |). Crystal's YAML parser drops unknown tags and hands back the tagged scalar as a plain string, so an inline-vault value already looks exactly like a vault-encrypted file's content by the time it reaches here - the same header check and decrypt() do the job. Recurses into arrays/hashes so a vault-encrypted value nested inside a list or mapping var is also caught.

Source
password
Source
password=(value : String | Nil)
Source
vault_ids
Source
yaml_value_to_json(value : YAML::Any) : JSON::Any

Converts a YAML::Any value to JSON::Any, recursively stringifying every hash key at every nesting level - unlike the JSON.parse( value.to_json) round-trip used in several places in this codebase, which crashes ("Can't convert Bool to a JSON object key") on a real, supported Ansible/Jinja2 idiom: a dict keyed by a bare YAML boolean (true:/false:/yes:/no:), which Python's own YAML loader happily parses as a bool-keyed dict and Jinja2 happily indexes with dict[some_bool_expression]. JSON has no non-string-key concept at all, so krikri-playbook's own JSON::Any-based variable representation stringifies the key ("true"/"false") instead - meaning a role that then indexes such a dict with a boolean expression needs that same "true"/"false" stringification applied at lookup time too (see VariableLookup's own dotted/bracket-index handling). Found benchmarking robertdebock.tailscale's own tailscale_sysctl_file/tailscale_command vars, both keyed by a bare boolean - the crash happened at PARSE time, before any task ran, taking down the entire playbook.

Source

Nested types