ansible_vault/
lib.rs

1//! Encrypt and decrypt Ansible Vault files
2//!
3//! This library provides methods to encrypt and decrypt ansible vault data, in 1.1 format
4//! It exposes six methods:
5//! * encrypt : Encrypt the input to a string without header `$ANSIBLE_VAULT;1.1;AES256` nor indentation,
6//! * encrypt_vault : Encrypt the input, and format like ansible (with header and indentation),
7//! * encrypt_vault_from_file : Encrypt the given file (wrapper for `encrypt_vault`)
8//! * decrypt : Decrypt a message string without header nor indentation
9//! * decrypt_vault : Decrypt a vault intput (with header and optionally indentation)
10//! * decrypt_vault_from file : Decrypt an ansible vault from file (wrapper for `decrypt_vault`)
11//!
12//! ## Usage
13//! Simple usage
14//!
15//! ```rust
16//! use ansible_vault::{encrypt_vault, decrypt_vault};
17//! let lipsum = "Lorem ipsum dolor…";
18//! let encoded = encrypt_vault(lipsum.as_bytes(),"5Up€rs3creT").unwrap();
19//! let decoded = decrypt_vault(encoded.as_bytes(), "5Up€rs3creT").unwrap();
20//! let decoded_str = String::from_utf8(decoded).unwrap();
21//! assert_eq!(lipsum, decoded_str);
22//! ```
23mod errors;
24
25pub use crate::errors::VaultError;
26use crate::errors::*;
27use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher};
28use aes_ctr::Aes256Ctr;
29use block_padding::{Padding, Pkcs7};
30use hmac::{Hmac, Mac, NewMac};
31use pbkdf2::pbkdf2;
32use rand::Rng;
33use sha2::Sha256;
34use std::fs::File;
35use std::io::{BufRead, Read};
36use std::path::Path;
37
38const VAULT_1_1_PREFIX: &str = "$ANSIBLE_VAULT;1.1;AES256";
39const AES_BLOCK_SIZE: usize = 16; // size in bytes
40const KEY_SIZE: usize = 32;
41
42type HmacSha256 = Hmac<Sha256>;
43
44/// Verify vault data with derived key2 and hmac authentication
45fn verify_vault(key: &[u8], ciphertext: &[u8], crypted_hmac: &[u8]) -> Result<()> {
46    let mut hmac = HmacSha256::new_varkey(key)?;
47    hmac.update(&ciphertext);
48    Ok(hmac.verify(crypted_hmac)?)
49}
50
51/// Generate derived keys and initialization vector from given key and salt
52fn generate_derived_key(
53    key: &str,
54    salt: &[u8],
55) -> ([u8; KEY_SIZE], [u8; KEY_SIZE], [u8; AES_BLOCK_SIZE]) {
56    let mut hmac_buffer = [0; 2 * KEY_SIZE + AES_BLOCK_SIZE];
57    pbkdf2::<HmacSha256>(key.as_bytes(), salt, 10000, &mut hmac_buffer);
58
59    let mut key1 = [0u8; KEY_SIZE];
60    let mut key2 = [0u8; KEY_SIZE];
61    let mut iv = [0u8; AES_BLOCK_SIZE];
62    key1.copy_from_slice(&hmac_buffer[0..KEY_SIZE]);
63    key2.copy_from_slice(&hmac_buffer[KEY_SIZE..2 * KEY_SIZE]);
64    iv.copy_from_slice(&hmac_buffer[2 * KEY_SIZE..2 * KEY_SIZE + AES_BLOCK_SIZE]);
65
66    (key1, key2, iv)
67}
68
69/// Decrypt ansible-vault payload (without header, no indentation nor carriage returns)
70///
71/// # Arguments
72/// * `input` : a data reader (&[u8], file, etc…) to the vault payload
73/// * `key` : the key to use decrypt
74///
75/// # Example
76/// ```rust, no_run
77///  # use ansible_vault::decrypt;
78///  let lipsum = "33666638363066623664653234386231616339646438303933633830376132633330353032393364\
79///                3363373531316565663539326661393165323030383934380a366133633066623963303665303238\
80///                34633364626339313035633763313034366538363537306265316532663531363632383333353737\
81///                3863616362363731660a666161623033666331663937626433313432616266393830376431393665\
82///                3965";
83///  let decoded = decrypt(lipsum.as_bytes(),"hush").unwrap();
84///  let decoded_str = String::from_utf8(decoded).unwrap();
85///  assert_eq!("lipsum", decoded_str);
86/// ```
87pub fn decrypt<T: Read>(mut input: T, key: &str) -> Result<Vec<u8>> {
88    // read payload
89    let mut payload = String::new();
90    input.read_to_string(&mut payload)?;
91    let unhex_payload = String::from_utf8(hex::decode(&payload)?)?;
92
93    // extract salt, hmac and crypted data
94    let mut lines = unhex_payload.lines();
95    let salt = hex::decode(
96        &lines
97            .next()
98            .ok_or_else(|| VaultError::from_kind(ErrorKind::InvalidFormat))?,
99    )?;
100    let hmac_verify = hex::decode(
101        &lines
102            .next()
103            .ok_or_else(|| VaultError::from_kind(ErrorKind::InvalidFormat))?,
104    )?;
105    let mut ciphertext = hex::decode(
106        &lines
107            .next()
108            .ok_or_else(|| VaultError::from_kind(ErrorKind::InvalidFormat))?,
109    )?;
110
111    // check data integrity
112    let (key1, key2, iv) = &generate_derived_key(key, salt.as_slice());
113    verify_vault(key2, &ciphertext, &hmac_verify)?;
114
115    // decrypt message
116    let mut cipher = Aes256Ctr::new_var(key1, iv)?;
117    cipher.apply_keystream(&mut ciphertext);
118    let n = Pkcs7::unpad(&ciphertext)?.len();
119    ciphertext.truncate(n);
120
121    Ok(ciphertext)
122}
123
124/// Decrypt an ansible vault formated stream
125///
126/// Message should be formatted with lines of 80 chars and indentation. Function expects header to
127/// be present but don't check format : lines of any length or any indentation will do
128/// ```text
129/// $ANSIBLE_VAULT;1.1;AES256
130///       33666638363066623664653234386231616339646438303933633830376132633330353032393364
131///       3363373531316565663539326661393165323030383934380a366133633066623963303665303238
132///       34633364626339313035633763313034366538363537306265316532663531363632383333353737
133///       3863616362363731660a666161623033666331663937626433313432616266393830376431393665
134///       3965
135///```
136/// # Arguments:
137/// * `input`: a stream of encrypted message with ansible-vault header
138/// * `key`: the key to decrypt the message
139pub fn decrypt_vault<T: Read>(input: T, key: &str) -> Result<Vec<u8>> {
140    let mut lines = std::io::BufReader::new(input).lines();
141    let first: String = lines
142        .next()
143        .ok_or_else(|| VaultError::from_kind(ErrorKind::NotAVault))??;
144
145    if first != VAULT_1_1_PREFIX {
146        return Err(VaultError::from_kind(ErrorKind::NotAVault));
147    }
148
149    let payload = lines
150        .filter_map(|i| i.ok())
151        .map(|s| s.trim().to_owned())
152        .collect::<Vec<String>>()
153        .join("");
154
155    decrypt(payload.as_bytes(), key)
156}
157
158/// Decrypt an ansible vault file using a key.
159///
160/// A wrapper for decrypt_vault method.
161///
162/// # Arguments:
163/// * `path`: the path to the encrypted vault file (&str, PathBuf, etc…)
164/// * `key`: the key to decrypt the file
165///
166pub fn decrypt_vault_from_file<P: AsRef<Path>>(path: P, key: &str) -> Result<Vec<u8>> {
167    let f = File::open(path)?;
168    decrypt_vault(f, key)
169}
170
171/// Encrypt a message to an ansible vault formated string
172///
173/// The output will be formatted with the ansible_vault header (1.1) an 80 chars lines and 6 spaces
174/// indentation.
175/// ```text
176/// $ANSIBLE_VAULT;1.1;AES256
177/// 33666638363066623664653234386231616339646438303933633830376132633330353032393364
178/// 3363373531316565663539326661393165323030383934380a366133633066623963303665303238
179/// 34633364626339313035633763313034366538363537306265316532663531363632383333353737
180/// 3863616362363731660a666161623033666331663937626433313432616266393830376431393665
181/// 3965
182///```
183/// # Arguments:
184/// * `input`: a stream to the data to encrypt
185/// * `key`: the key to encrypt the message
186pub fn encrypt_vault<T: Read>(input: T, key: &str) -> Result<String> {
187    let line_length = 80;
188    let ciphertext = encrypt(input, key)?;
189    let mut buffer = Vec::new();
190    for chunk in ciphertext.into_bytes().chunks(line_length) {
191        let mut line = [chunk, "\n".as_bytes()].concat();
192        buffer.append(&mut line);
193    }
194
195    let vault_text = format! {"{}\n{}", VAULT_1_1_PREFIX, String::from_utf8(buffer)?};
196
197    Ok(vault_text)
198}
199
200/// Encrypt message to string without formatting (no header, no carriage returns)
201///
202/// # Arguments
203/// * `input` : a data reader (&[u8], file, etc…) to the message
204/// * `key` : the key to use encrypt
205///
206/// # Example
207/// ```rust, no_run
208///  # use ansible_vault::encrypt;
209///  let lipsum = "Lorem ipsum dolor";
210///  let decoded = encrypt(lipsum.as_bytes(),"hush").unwrap();
211/// ```
212pub fn encrypt<T: Read>(mut input: T, key: &str) -> Result<String> {
213    // Pad input data
214    let mut buffer = Vec::new();
215    input.read_to_end(&mut buffer)?;
216    let pos = buffer.len();
217    let pad_len = AES_BLOCK_SIZE - (pos % AES_BLOCK_SIZE);
218    buffer.resize(pos + pad_len, 0);
219    let mut block_buffer = Pkcs7::pad(buffer.as_mut_slice(), pos, AES_BLOCK_SIZE)?;
220
221    // Derive cryptographic keys
222    let salt = rand::thread_rng().gen::<[u8; 32]>();
223    let (key1, key2, iv) = &generate_derived_key(key, &salt);
224
225    // Encrypt data
226    let mut cipher = Aes256Ctr::new_var(key1, iv)?;
227    cipher.apply_keystream(&mut block_buffer);
228
229    // Message authentication
230    let mut mac = HmacSha256::new_varkey(key2)?;
231    mac.update(block_buffer);
232    let result = mac.finalize();
233    let b_hmac = result.into_bytes();
234
235    // Format data
236    let ciphertext = format!(
237        "{}\n{}\n{}",
238        hex::encode(salt),
239        hex::encode(b_hmac),
240        hex::encode(block_buffer)
241    );
242
243    Ok(hex::encode(ciphertext))
244}
245
246/// Encrypt a file to an ansible_vault string
247///
248/// A wrapper for encrypt_vault method.
249///
250/// # Arguments:
251/// * `path`: the path to the file to encrypt
252/// * `key`: the key to encrypt the file
253///
254pub fn encrypt_vault_from_file<P: AsRef<Path>>(path: P, key: &str) -> Result<String> {
255    let f = File::open(path)?;
256    encrypt_vault(f, key)
257}
258
259#[cfg(test)]
260mod tests {
261    use crate::errors::{ErrorKind, VaultError};
262    use std::fs;
263
264    const LIPSUM_PATH: &str = "./test/lipsum.txt";
265    const LIPSUM_VAULT_PATH: &str = "./test/lipsum.vault";
266    const LIPSUM_SECRET: &str = "shibboleet";
267
268    #[test]
269    fn test_wrong_password() {
270        let result = crate::decrypt_vault_from_file(LIPSUM_VAULT_PATH, "p@$$w0rd").unwrap_err();
271        assert_eq!(result, VaultError::from_kind(ErrorKind::IncorrectSecret));
272    }
273
274    #[test]
275    fn test_decrypt() {
276        let buf = crate::decrypt_vault_from_file(LIPSUM_VAULT_PATH, LIPSUM_SECRET).unwrap();
277        let lipsum = String::from_utf8(buf).unwrap();
278        let reference = fs::read_to_string(LIPSUM_PATH).unwrap();
279        assert_eq!(lipsum, reference);
280    }
281
282    #[test]
283    fn test_encrypt() {
284        let lipsum = fs::read_to_string(LIPSUM_PATH).unwrap();
285        let encoded = crate::encrypt_vault_from_file(LIPSUM_PATH, LIPSUM_SECRET).unwrap();
286        let decoded = crate::decrypt_vault(encoded.as_bytes(), LIPSUM_SECRET).unwrap();
287        let decoded_str = String::from_utf8(decoded).unwrap();
288        assert_eq!(lipsum, decoded_str);
289    }
290
291}